From 0f7101983e0960485130ee48bfce27c6fc4d237b Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 30 Sep 2026 12:37:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(msgcenter):=20/msg=20=E6=8D=A2=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=95=B0=E6=8D=AE=E2=80=94=E2=80=94=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E7=AD=9B=E9=80=89+=E6=BB=9A=E5=8A=A8=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E3=80=81=E9=93=83=E9=93=9B=E7=9B=B4=E8=B7=B3=E3=80=81?= =?UTF-8?q?unreadCount=20=E4=B8=8A=E6=8F=90=20store=E3=80=81WS=20=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=B8=A6=E5=BA=94=E7=94=A8=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - views/msg/index.vue:数据层整体替换(msgType 0/1/2 映射、isRead 1=已读、fmtTime 重写绝对时间);列表走 msgCenter/list 服务端筛选(msgType/appKey/isRead/keyword 防抖 300ms),滚动到底追加下一页,切视图/搜索重置第 1 页;左栏分类计数走 summary(旧版 UserMsg 缺字段降级显示 —),应用清单取 store 应用列表、计数同源 todos;点击卡片标已读、全部已读、去处理(showJump/jumpUrl)、审批直批(共享弹窗);失败态有重试;WS 新消息经 mittBus 自刷新 - components/Header.vue:铃铛直跳 /msg(移除 Poptip 浮层与 900 宽 Modal 及 columns/tableApi 死代码);unreadCount 上提 pinia store(角标与 /msg、LeftMenu 同源);WS Notice.info 标题带应用名(appKey 匹配 store 应用列表,缺失回退类型标签);壁纸/LOGO Modal 不动 - components/MsgApprovalModal.vue(新增):审批直批弹窗抽共享组件,逻辑照抄原 Header 内联实现(同意可选意见/拒绝必填校验/三种失败反馈) - components/LeftMenu.vue:「消息」入口(url=/msg)未读角标(读 store,红底白字胶囊) - store/index.ts:unreadCount + setUnreadCount/incUnread/decUnread - api/BaseApi.ts:getMsgCenterList 参数类型放宽(服务端筛选项);getMsgCenterSummary 返回类型补分类计数字段(可缺省) - assets/styles/header.scss:删除消息浮层死样式 .message-wrap 段 --- src/api/BaseApi.ts | 18 +- src/assets/styles/header.scss | 93 ------- src/components/Header.vue | 340 +++--------------------- src/components/LeftMenu.vue | 22 ++ src/components/MsgApprovalModal.vue | 89 +++++++ src/store/index.ts | 12 + src/views/msg/index.vue | 391 ++++++++++++++++++++-------- 7 files changed, 451 insertions(+), 514 deletions(-) create mode 100644 src/components/MsgApprovalModal.vue diff --git a/src/api/BaseApi.ts b/src/api/BaseApi.ts index e517d47..6992e20 100644 --- a/src/api/BaseApi.ts +++ b/src/api/BaseApi.ts @@ -98,21 +98,31 @@ class Base extends Abstract { // ===== 消息中心代理(§3.5:经后端签名代理 UserMsg,前端不直连)===== /** - * 消息中心分页列表({total, list[]};元素含改写后的绝对 jumpUrl) + * 消息中心分页列表({total, list[]};元素含改写后的绝对 jumpUrl)。 + * 可选服务端筛选:msgType 0/1/2、appKey、isRead 0/1、keyword 标题/内容模糊。 * @memberof Basic */ - getMsgCenterList(params: PageParams) { + getMsgCenterList(params: PageParams & { msgType?: number; appKey?: string; isRead?: number; keyword?: string }) { const url = path + "/msgCenter/list"; return this.getRequest({ url, params }); } /** - * 铃铛摘要:未读数 + 各应用待办角标 + * 铃铛摘要:未读数 + 各应用待办角标 + 分类计数(sysUnread/bizUnread/approvalUnread/total/readCount, + * 旧版 UserMsg 无 summary 端点时这些字段缺省,前端按降级处理) * @memberof Basic */ getMsgCenterSummary() { const url = path + "/msgCenter/summary"; - return this.getRequest<{ unreadCount: number; todos: { appKey: string; count: number }[] }>({ url }); + return this.getRequest<{ + unreadCount: number; + todos: { appKey: string; count: number }[]; + sysUnread?: number; + bizUnread?: number; + approvalUnread?: number; + total?: number; + readCount?: number; + }>({ url }); } /** diff --git a/src/assets/styles/header.scss b/src/assets/styles/header.scss index f4a5065..9fa2b63 100644 --- a/src/assets/styles/header.scss +++ b/src/assets/styles/header.scss @@ -150,99 +150,6 @@ } } -.message-wrap { - h3.title { - height: 50px; - padding: 12px 16px; - color: #272829; - font-weight: 600; - font-size: 16px; - border-bottom: 1px solid #e8eaec; - } - .msg-ul { - width: 360px; - background: #fff; - padding-bottom: 0; - border-radius: 10px; - max-height: 240px; - min-height: 100px; - li { - height: 46px; - padding: 5px 18px; - position: relative; - overflow: hidden; - line-height: 36px; - border-bottom: 1px dotted #e8eaec; - - h4 { - width: calc(100% - 40px); - float: left; - font-size: 15px; - color: #333; - font-weight: 500; - } - .content { - border: none; - font-size: 13px; - cursor: pointer; - } - .read { - color: #999999; - } - a { - float: right; - height: 32px; - outline: none; - display: flex; - align-items: center; - } - svg { - width: 20px; - height: 20px; - } - } - .no-msg-box { - display: flex; - height: 100px; - justify-content: center; - align-items: center; - border: none; - } - .app-name-box { - border: none; - color: #fff !important; - background: #577afb; - border-radius: 3px; - padding: 6px 8px; - font-size: 13px; - margin-right: 12px; - } - - .fix a img { - width: 22px; - height: 22px; - margin-top: 5px; - } - } - .footer { - background-color: #f9f9f9; - height: 54px; - line-height: 54px; - padding: 0 20px; - - font-size: 15px; - border-top: 1px solid #e8eaec; - - a.link-all { - color: #999999; - font-weight: 550; - } - a.read-all { - color: #5979ea; - } - } -} - .role-info { height: 22px; color: #6b8cf4 !important; diff --git a/src/components/Header.vue b/src/components/Header.vue index d7458a7..9783867 100644 --- a/src/components/Header.vue +++ b/src/components/Header.vue @@ -23,58 +23,14 @@ +
  • - -
    - - - -
    - -
    +
    + + + +
  • - -
    - - -
    - - -
    @@ -277,21 +222,8 @@ - - -
    -

    {{ approval.title || "确认办理该待办?" }}

    - -
    - -
    + + @@ -304,6 +236,7 @@ import SvgIcon from "@/components/SvgIcon.vue"; import vuescroll from "vuescroll"; import { ref, reactive, computed, onMounted, onUnmounted, inject, watch } from "vue"; import { useDefaultStore } from "@/store"; +import { storeToRefs } from "pinia"; import { useRoute, useRouter } from "vue-router"; import { requireImg } from "@/utils/hook/useImg"; import ContextMenu from "@/components/ContextMenu.vue"; @@ -311,11 +244,10 @@ import MenuBarModal from "@/components/MenuBarModal.vue"; import StatsBarModal from "@/components/StatsBarModal.vue"; import TeacherCopilotModal from "@/components/TeacherCopilotModal.vue"; import BaseApi from "@/api/BaseApi"; -import TablePage from "@/components/TablePage.vue"; +import MsgApprovalModal from "@/components/MsgApprovalModal.vue"; import { Notice } from "view-ui-plus"; import { IframePageParmas, RoleInfo } from "@/types"; import { isEqual, orderBy } from "lodash"; -import { PageParams } from "@/types/Basics"; import UserSettingApi from "@/api/UserSettingApi"; import mittBus from "@/utils/mitt"; import MenuBarApi from "@/api/MenuBarApi"; @@ -324,10 +256,13 @@ import Utils from "@/utils"; const route = useRoute(); const router = useRouter(); const store = useDefaultStore(); -const refTablePage: any = ref(null); +const refMsgApproval = ref(); const { $conf } = useCurrentInstance(); const baseUrl = computed(() => $conf.fileHost); +/** 未读消息数上提 pinia store(消息对接 2026-09-30):Header 角标与 /msg 页左栏、LeftMenu 同源 */ +const { unreadCount } = storeToRefs(store); + const refSearchInput = ref(); const refMenuBarModal = ref(); const refStatsBarModal = ref(); @@ -341,19 +276,14 @@ const logoUrl = computed(() => { const sysLogo = logoList.value.find(x => x.type == 0); return sysLogo ? sysLogo.url : ""; }); -const isMsgShow = ref(false), - isMsgListShow = ref(false), - isUserInfoShow = ref(false), +const isUserInfoShow = ref(false), isRoleInfoShow = ref(false), isWallpaperShow = ref(false), isLogoShow = ref(false), isSearching = ref(false), searchAppText = ref(null), wallpaperList = ref([]), - logoList = ref([]), - userMsgList = ref([]), - /** 未读消息数(msgCenter/summary 拉取 + WS newMessage 实时累加) */ - unreadCount = ref(0); + logoList = ref([]); // ===== 消息中心 WS 实时推送(UserMsg /ws/msg,一次性票据握手;断线 5s 重连换新票) ===== let msgWs: WebSocket | null = null; @@ -364,7 +294,7 @@ let msgWsClosed = false; async function loadSummary() { try { const summary = await BaseApi.getMsgCenterSummary(); - unreadCount.value = (summary && summary.unreadCount) || 0; + store.setUnreadCount(summary && summary.unreadCount); } catch (e) { /* 摘要失败不打扰页面 */ } @@ -415,12 +345,22 @@ async function connectMsgWs() { }; } +/** WS 帧按 appKey 匹配应用名(store 应用列表);查无回退类型标签(缺失降级,不报错) */ +function frameAppName(frame: any) { + const appKey = frame && frame.appKey; + if (appKey) { + const hit = store.getFlattenAppList.find((x: any) => x.appKey == appKey || x.appCode == appKey); + if (hit && hit.name) return hit.name; + } + return msgTypeLabel(frame ? frame.msgType : null); +} + function onNewMessage(frame: any) { - unreadCount.value += 1; - reloadMsg(); - if (isMsgListShow.value) resetMsgPage(); + store.incUnread(); + // /msg 页开着时自刷新(页内监听该事件重拉第一页与摘要) + mittBus.emit("msgCenter:newMessage", frame); Notice.info({ - title: frame.title || "新消息", + title: `【${frameAppName(frame)}】新消息`, desc: frame.content || "", duration: 5, }); @@ -461,107 +401,8 @@ const userRole = computed(() => store.userRoleList); const toggleSearch: any = inject("toggleSearch"); -const pageParams: PageParams = reactive({ page: 1, count: 10 }); -const tableApi = (params: PageParams) => BaseApi.getMsgCenterList(params); -const columns = ref([ - { - type: "index", - title: "序号", - width: 70, - align: "center", - }, - { - title: "应用", - width: 110, - align: "center", - render(h: any, params: any) { - return h("span", params.row.appName || msgTypeLabel(params.row.msgType)); - }, - }, - { - title: "消息内容", - render(h: any, params: any) { - const row = params.row; - return h("span", { attrs: { title: row.content } }, `${row.title || ""}:${row.content || ""}`); - }, - }, - { - title: "状态", - width: 100, - render(h: any, params: any) { - return h("span", params.row.isRead === 1 ? "已读" : "未读"); - }, - }, - { - title: "日期", - align: "center", - width: 200, - key: "sendTime", - }, - { - title: "操作", - width: 210, - align: "center", - render(h: any, params: any) { - const row = params.row; - const nodes: any[] = []; - // 审批待办(msgType=2)直批入口:taskId 由服务端按 bizId 匹配定位; - // 无权限/已处理由后端返回文案,直批按钮不受跳转权限影响 - if (row.msgType === 2 && row.msgId) { - nodes.push( - h( - "a", - { - attrs: { href: "javascript:;" }, - on: { - click: () => openApproval(row, "approve"), - }, - }, - "同意" - ), - h("span", " "), - h( - "a", - { - attrs: { href: "javascript:;" }, - on: { - click: () => openApproval(row, "reject"), - }, - }, - "拒绝" - ) - ); - } - if (row.showJump) { - if (nodes.length) nodes.push(h("span", " ")); - nodes.push( - h( - "a", - { - attrs: { href: "javascript:;" }, - on: { - click: () => { - if (row.isRead !== 1 && row.msgId) { - BaseApi.readMsgCenter(row.msgId); - row.isRead = 1; - unreadCount.value = Math.max(0, unreadCount.value - 1); - } - window.open(row.jumpUrl, "_blank"); - }, - }, - }, - "去处理" - ) - ); - } - if (!nodes.length) return h("span", "—"); - return h("span", nodes); - }, - }, -]); onMounted(async () => { setTimeout(() => { - reloadMsg(); loadSummary(); connectMsgWs(); // isWallpaperShow.value = true; @@ -610,94 +451,11 @@ function onUserinfoClickRoles() { }, 0); } -async function readMsgAll(type: 1 | 2) { - await BaseApi.readMsgCenterAll(); - await reloadMsg(); - unreadCount.value = 0; - if (type == 2) resetMsgPage(); -} - -async function reloadMsg() { - const result: any = await BaseApi.getMsgCenterList({ page: 1, count: 10 }); - userMsgList.value = result.list || []; -} - -/** 消息类型名(UserMsg msgType:0系统通知/1业务提醒/2审批待办) */ -function msgTypeLabel(msgType: number) { +/** 消息类型名(UserMsg msgType:0系统通知/1业务提醒/2审批待办);WS 帧缺 appName 时的降级标签 */ +function msgTypeLabel(msgType: number | null) { return { 0: "系统通知", 1: "业务提醒", 2: "审批待办" }[msgType as 0 | 1 | 2] || "系统通知"; } -/** 点击消息:未读先标读;配了 jumpUrl(如 flow.task 待办)新窗口打开落地页 */ -async function onClickMsg(item: any) { - if (item.isRead !== 1 && item.msgId) { - await BaseApi.readMsgCenter(item.msgId); - item.isRead = 1; - unreadCount.value = Math.max(0, unreadCount.value - 1); - } - if (item.jumpUrl) window.open(item.jumpUrl, "_blank"); -} - -// ===== 卡片直批(B3 §2):同意/拒绝待办;operatorId/schId/operatorName 服务端取会话 ===== -const approval = reactive({ - visible: false, - msgId: 0, - action: "approve" as "approve" | "reject", - title: "", - opinion: "", - submitting: false, -}); - -function openApproval(item: any, action: "approve" | "reject") { - approval.msgId = item.msgId; - approval.action = action; - approval.title = item.title ? String(item.title) : ""; - approval.opinion = ""; - approval.visible = true; -} - -async function submitApproval() { - if (approval.submitting) return; - if (approval.action === "reject" && !approval.opinion.trim()) { - Notice.warning({ title: "拒绝必须填写意见", duration: 3 }); - return; - } - approval.submitting = true; - try { - const res: any = await BaseApi.handleMsg(approval.msgId, approval.action, approval.opinion.trim()); - if (res && res.ok) { - Notice.success({ title: "办理成功", duration: 3 }); - approval.visible = false; - markRowRead(approval.msgId); - } else { - Notice.warning({ title: (res && res.message) || "办理失败", duration: 5 }); - } - // 成功/失效/被他人处理等场景都同步刷新,让卡片状态跟上服务端 - reloadMsg(); - } catch (e) { - // flow-center 不可达等:降级为提示,不阻塞消息卡片其余功能 - Notice.error({ title: "流程中心暂不可用,请稍后重试", duration: 5 }); - } finally { - approval.submitting = false; - } -} - -function markRowRead(msgId: number) { - const row = userMsgList.value.find((m: any) => m.msgId === msgId); - if (row && row.isRead !== 1) { - row.isRead = 1; - unreadCount.value = Math.max(0, unreadCount.value - 1); - } -} - -function resetMsgPage() { - refTablePage.value.reloadData(); -} -function showMsgList() { - isMsgShow.value = false; - router.push("/msg"); - refTablePage.value.reloadData(); -} - async function wallpaperSuccess(response: any, file: any, fileListt: any) { await getUserWallpaperList(); } @@ -802,36 +560,6 @@ function logout() { diff --git a/src/store/index.ts b/src/store/index.ts index 0ceeb82..1c1f9e9 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -13,6 +13,8 @@ export interface IdefaultStore { curUserMenuList: Array; //当前角色菜单 curUserAppList: Array; //当前角色所有应用 curRoleUid: string | null; //当前角色uid + /** 未读消息数(msgCenter/summary 拉取 + WS newMessage 实时累加;Header 角标与 /msg 页左栏同源) */ + unreadCount: number; // curUsedAppList: Array; //当前角色常用应用 } @@ -50,12 +52,22 @@ export const useDefaultStore = defineStore("defaultStore", { curUserAppList: [], curUserMenuList: [], curRoleUid: null, + unreadCount: 0, }; }, actions: { setCurRoleInfo(info: RoleInfo) { this.curRoleInfo = info; }, + setUnreadCount(count: number) { + this.unreadCount = Math.max(0, count || 0); + }, + incUnread() { + this.unreadCount += 1; + }, + decUnread() { + this.unreadCount = Math.max(0, this.unreadCount - 1); + }, setCurUserSettingsInfo(info: UserSettingInfo) { this.curUserSettingInfo = info; }, diff --git a/src/views/msg/index.vue b/src/views/msg/index.vue index 1a4fa29..fb4df88 100644 --- a/src/views/msg/index.vue +++ b/src/views/msg/index.vue @@ -23,18 +23,18 @@ - +
    全部应用
    - 全部{{ msgs.length }} + 全部{{ summaryTotalHint }}
    {{ a.name }}{{ a.cnt }}
    @@ -46,12 +46,12 @@

    {{ curTitle }}

    - 未读 {{ unreadTotal }} 条 + 未读 {{ unreadCount }} 条
    - + - +
    消息样式
    @@ -62,82 +62,83 @@
    -
    +
    -
    +
    +
    ⚠️
    +
    消息加载失败
    + +
    +
    ✅
    暂无消息
    -
    - {{ typeIcon(m.t) }} +
    + {{ typeIcon(m.msgType) }}
    - {{ m.app }} - {{ typeName(m.t) }} + {{ m.appName || typeAppName(m) }} + {{ typeName(m.msgType) }}
    -
    {{ m.tt }}{{ m.body }}
    +
    {{ m.title }}{{ m.content }}
    - {{ fmtTime(m.min) }} + {{ fmtTime(m.sendTime) }}
    - @@ -424,6 +584,7 @@ function fmtTime(min: number) { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); transition: all 0.2s; overflow: hidden; + cursor: pointer; &:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); border-color: #dfe3ea; @@ -442,10 +603,10 @@ function fmtTime(min: number) { &.unread::before { background: #5979ea; } - &.unread.m2::before { + &.unread.mbiz::before { background: #e9b954; } - &.unread.m3::before { + &.unread.mappr::before { background: #e8684a; } .row1 { @@ -672,4 +833,12 @@ function fmtTime(min: number) { opacity: 0.5; } } + +/* 滚动分页脚标(加载中 / 没有更多) */ +.loading-more { + text-align: center; + padding: 10px 0 14px; + font-size: 12px; + color: #b3b3b3; +}