Browse Source

Web 顶部消息通知接入真实接口

- 改造 src/layout/components/message.vue:未读红点、未读下拉、点击查看即已读、全部已读,字段按 title/content/createTime/isRead

- 新增 src/api/message/index.js:message 中心 5 个接口封装

- 沿用 MQTT 订阅 sys/{userId}/msg 实时刷新

- 文档补充 App 消息推送对接任务;同步更新 PAGES_TODO/CURRENT_STATUS/DEVELOPMENT_LOG

- 提交前 eslint 与 build:prod 通过
master
DU 4 days ago
parent
commit
7699ef41d0
  1. 1
      docs/development/CURRENT_STATUS.md
  2. 9
      docs/development/DEVELOPMENT_LOG.md
  3. 2
      docs/development/PAGES_TODO.md
  4. 26
      src/api/message/index.js
  5. 192
      src/layout/components/message.vue

1
docs/development/CURRENT_STATUS.md

@ -29,6 +29,7 @@ @@ -29,6 +29,7 @@
- 生产构建脚本已加入 Node 22/OpenSSL 3 兼容参数。
- `api.json` 已生成 28 个模块、278 个接口和 32 个模型的本地文档(2026-09-08 重新生成)。
- 登录成功且无指定回跳地址时,默认进入隐患台账 `/hazard/ledger`;根路径也统一重定向至该页面,并以隐藏基础路由保证首次登录时可解析。
- 顶部「消息」通知已接入真实接口(2026-10-15):未读数徽标 `message/unread-count`、未读下拉 `message/list`、点击查看即已读 `message/detail/{id}`、全部已读 `message/readAll`;并沿用 MQTT(订阅 `sys/{userId}/msg`)实时刷新。原 `setMessageNum` 空实现已由组件内真实请求取代。
## 重点业务页面状态

9
docs/development/DEVELOPMENT_LOG.md

@ -1,5 +1,14 @@ @@ -1,5 +1,14 @@
# 开发日志
## 2026-10-15:Web 顶部消息通知接入真实接口
- 改造 `src/layout/components/message.vue`:头部铃铛下拉接入真实接口——未读数徽标 `message/unread-count`、未读下拉 `message/list`、点击查看即已读 `message/detail/{id}`、底部「全部已读」`message/readAll`;字段按 `title/content/createTime/isRead` 渲染并只展示未读。
- 新增 `src/api/message/index.js`:5 个消息中心接口封装(`unread-count`/`list`/`detail/{id}`/`read/{id}`/`readAll`)。
- 实时提醒:沿用 MQTT(`MyConfig.MQTTconnection`,订阅 `sys/{userId}/msg`),收到消息即刷新未读;组件卸载时 `client.end(true)`。
- 原 `store` 的 `setMessageNum` 空实现保留(不再被组件使用)。
- 待确认:点击消息的业务跳转规则(按 `msgType`+`bizId`),当前仅显示详情;后端需按 `sys/{userId}/msg` 主题推送。
- 验证:`eslint --no-ignore` 通过、`npm run build:prod` 通过(仅既有 warning)。
## 2026-10-15:新增 Web 风控与审计页面(W8)
- 新增 `src/views/risk/index.vue` + `src/api/risk/index.js`,两个 Tab:

2
docs/development/PAGES_TODO.md

@ -56,6 +56,7 @@ @@ -56,6 +56,7 @@
| 32 | 系统管理 | 部门管理 | ✅ | ✅ | — | [system/dept](../../src/views/system/dept/index.vue) |
| 33 | 系统管理 | 岗位管理 | ✅ | ✅ | — | [system/post](../../src/views/system/post/index.vue) |
| 34 | 系统管理 | 字典管理 | ✅ | ✅ | — | [system/dict](../../src/views/system/dict/index.vue) |
| — | 顶部 | 消息通知(头部铃铛:未读红点 + 未读下拉 + 查看即已读 + 全部已读) | ⚠️ 待测试 | ✅ | ✅ | **已开发(2026-10-15)**:`src/layout/components/message.vue` + `src/api/message/index.js`。接口 `message/unread-count`·`list`·`detail/{id}`·`readAll`;MQTT 实时刷新(订阅 `sys/{userId}/msg`,需后端推送) |
| — | 系统监控 | 在线用户/定时任务/调度日志/缓存/服务/操作日志/登录日志 | ✅ | ✅ | — | [monitor/](../../src/views/monitor)(旧模块对接) |
| — | — | 个人中心 / 账号退出 | ✅ | ✅ | — | 已实现 |
@ -84,6 +85,7 @@ @@ -84,6 +85,7 @@
| 13 | 我的 | 消息中心(列表/详情/已读) | ✅ | ✅ | — | [MessagesScreen](../../../zhongyouApp/src/screens/home/MessagesScreen.tsx) |
| 14 | 商城 | 商城-首页 | ❌ | ⛔ | ⛔ | 惠服商城页面本体(三方),见下方原因 |
| 15 | 商城 | 商城-商品详情 / 购物车 / 确认订单 / 商城个人中心 | ❌ | ⛔ | ⛔ | 同上(4 屏) |
| 16 | 消息 | 消息推送对接(App 内实时提醒) | ❌ | ⚠️ | ⚠️ | **对接信息**:MQTT 主题 `sys/{userId}/msg`(服务见 `public/static/config.js` 的 `MQTTconnection`,Web 端已接入);接口 `message/unread-count`·`list`·`detail/{id}`·`readAll`。App 需接入 MQTT 客户端做全局新消息提醒;**推送主题与 App 端连接方案需后端确认** |
> 备注:APP 现状已含“隐患录入照片上传≤8 张”(`uploadHazardAttachment`),[zhongyouApp/开发任务清单.md](../../../zhongyouApp/开发任务清单.md) 中“照片未做”已过时。

26
src/api/message/index.js

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
import request from '@/utils/request'
// 未读消息数量(头部红点)
export function getUnreadMessageCount() {
return request({ url: '/business/message/unread-count', method: 'get' })
}
// 我的消息列表(分页)
export function listMessages(params) {
return request({ url: '/business/message/list', method: 'get', params })
}
// 消息详情(查看即标记已读)
export function getMessageDetail(id) {
return request({ url: '/business/message/detail/' + id, method: 'get' })
}
// 标记单条已读
export function readMessage(id) {
return request({ url: '/business/message/read/' + id, method: 'post' })
}
// 全部标记已读
export function readAllMessages() {
return request({ url: '/business/message/readAll', method: 'post' })
}

192
src/layout/components/message.vue

@ -1,11 +1,6 @@ @@ -1,11 +1,6 @@
<!--
* @author: fu_li_wei
* @since: 2025-05-12
* message.vue
-->
<template>
<el-dropdown ref="dropdownRef" class="right-menu-message" trigger="click">
<el-badge :value="$store.getters.messageNum">
<el-badge :value="$store.getters.messageNum" :hidden="!$store.getters.messageNum">
<el-button size="small" icon="el-icon-bell" type="info" round plain>消息</el-button>
</el-badge>
<el-dropdown-menu slot="dropdown">
@ -14,115 +9,118 @@ @@ -14,115 +9,118 @@
<span>未读消息({{ $store.getters.messageNum }})</span>
</div>
<div class="message-body">
<div v-for="item in messageList" :key="item.id" class="message-content">
<span class="message-content-content">{{ item.content }}</span>
<span class="message-content-date">{{ item.date }}</span>
<div v-for="item in messageList" :key="item.id" class="message-content" @click="openMessage(item)">
<span class="message-content-content">
<span class="message-dot" />
{{ item.title || item.content || '消息' }}
</span>
<span class="message-content-date">{{ formatTime(item.createTime) }}</span>
</div>
</div>
<el-divider></el-divider>
<el-divider />
<div class="message-foot items-center">
<span @click="loadingMore">查看更多</span>
<span @click="readAll">全部已读</span>
<svg-icon icon-class="link" style="font-size: 10px;" />
</div>
</div>
<div v-else class="dropdown-message">
<el-empty :image-size="50" description="暂无消息"></el-empty>
<el-empty :image-size="50" description="暂无未读消息" />
</div>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
/* global MyConfig */
import mqtt from 'mqtt'
// const client = mqtt.connect('ws://221.219.102.234:8083/mqtt', {
// clientId: 'mqttjs_' + Math.random().toString(16).substr(2, 8),
// })
// client.on('connect', () => {
// console.log('Connected')
// })
// client.on('error', error => {
// console.log('Connection failed', error)
// })
// // 订阅主题
// client.subscribe('sound-alarm', { qos: 0 }, (error, res) => {})
import { getUnreadMessageCount, listMessages, getMessageDetail, readAllMessages } from '@/api/message'
export default {
name: 'Message',
components: {},
data() {
return {
client: null,
messageList: []
}
},
async created() {
created() {
this.createConnection()
const list = await this.$store.dispatch('setMessageNum')
this.messageList = list
this.refresh()
},
mounted() {
// const _that = this
// client.on('message', (topic, message) => {
// console.log('收到消息:', topic, JSON.parse(message));
// switch (topic) {
// case 'warn-zj':
// _that.newMessage(JSON.parse(message))
// break;
// }
// this.$store.commit('setUntreated')
// })
beforeDestroy() {
if (this.client) {
this.client.end(true)
this.client = null
}
},
methods: {
/** 链接Mqtt服务 */
/** 拉取未读数量与未读消息列表 */
async refresh() {
try {
const [countRes, listRes] = await Promise.all([
getUnreadMessageCount(),
listMessages({ pageNum: 1, pageSize: 10 })
])
this.$store.commit('SET_MESSAGE_NUM', Number(countRes.data || 0))
const page = listRes.data || listRes
const list = page.list || page.rows || []
this.messageList = list.filter(item => String(item.isRead) !== '1')
} catch (error) {
this.$store.commit('SET_MESSAGE_NUM', 0)
this.messageList = []
}
},
/** 点击消息:查看详情(即已读)并刷新 */
openMessage(item) {
if (!item) return
if (String(item.isRead) === '1') return
getMessageDetail(item.id)
.then(response => {
const detail = response.data || item
this.$alert(detail.content || '', detail.title || '消息详情', { confirmButtonText: '知道了' })
this.refresh()
})
.catch(() => {
this.$modal.msgError('消息详情加载失败')
})
},
/** 全部已读 */
readAll() {
readAllMessages()
.then(() => {
this.$modal.msgSuccess('已全部标记为已读')
this.refresh()
})
.catch(() => {
this.$modal.msgError('操作失败,请稍后重试')
})
},
/** 连接 MQTT,订阅当前用户消息主题 */
createConnection() {
const _that = this
const { host, port, endpoint, ...options } = MyConfig.MQTTconnection;
// 连接
const { host, port, endpoint, ...options } = MyConfig.MQTTconnection
this.client = mqtt.connect(`ws://${host}:${port}${endpoint}`, options)
// 成功
this.client.on('connect', () => {
/**
* 订阅主题
* 订阅的主题应该和 message接受配合使用
*/
_that.client.subscribe(`sys/${_that.$store.getters.user_id}/msg`, { qos: 0 }, (error, res) => {
_that.client.subscribe(`sys/${_that.$store.getters.user_id}/msg`, { qos: 0 }, () => {
console.log(`订阅主题: sys/${_that.$store.getters.user_id}/msg 成功`)
});
_that.client.subscribe(`test`, { qos: 0 }, (error, res) => {
console.log(`订阅主题: test 成功`)
});
_that.client.subscribe(`sys/test/msg`, { qos: 0 }, (error, res) => {
console.log(`订阅主题: sys/test/msg 成功`)
});
})
})
// 失败
this.client.on('error', error => {
console.log('Connection failed', error)
})
// 接收消息
this.client.on('message', (topic, message) => {
console.log('收到消息:', topic, message.toString())
switch (topic) {
case `sys/${_that.$store.getters.user_id}/msg`:
_that.newMessage()
break;
if (topic === `sys/${_that.$store.getters.user_id}/msg`) {
_that.refresh()
}
})
},
/** 跳转信息页 */
loadingMore() {
setTimeout(() => {
this.$refs.dropdownRef.hide()
}, 300)
},
/** 新的信息 */
async newMessage(data) {
const list = await this.$store.dispatch('setMessageNum')
this.messageList = list
formatTime(value) {
if (!value) return ''
return String(value).replace('T', ' ').slice(5, 16)
}
}
}
@ -142,31 +140,13 @@ export default { @@ -142,31 +140,13 @@ export default {
}
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
::v-deep .el-badge__content {
line-height: 15px;
}
.dropdown-message-more {
width: 300px;
width: 320px;
.message-head {
width: 100%;
height: 30px;
@ -182,10 +162,12 @@ export default { @@ -182,10 +162,12 @@ export default {
}
.message-body {
// height: 200px;
// overflow-y: auto;
max-height: 300px;
overflow-y: auto;
.message-content {
padding: 6px 15px;
padding: 8px 15px;
cursor: pointer;
&:hover {
background-color: #fafafa;
@ -194,12 +176,11 @@ export default { @@ -194,12 +176,11 @@ export default {
.message-content-content {
display: block;
font-size: 13px;
// 一行显示,超出隐藏,用省略号表示
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.message-content-date {
font-size: 12px;
color: #999;
@ -213,6 +194,7 @@ export default { @@ -213,6 +194,7 @@ export default {
color: #1890ff;
font-size: 12px;
font-weight: 500;
cursor: pointer;
span {
margin-right: 4px;
}
@ -222,20 +204,20 @@ export default { @@ -222,20 +204,20 @@ export default {
margin: 10px 0;
}
.text-gray {
font-size: 13px;
color: #999;
}
.items-center {
display: flex;
align-items: center;
}
}
.justify-between {
display: flex;
justify-content: space-between;
}
.message-dot {
display: inline-block;
width: 6px;
height: 6px;
margin-right: 4px;
border-radius: 50%;
background: #d64550;
vertical-align: middle;
}
.dropdown-message {

Loading…
Cancel
Save