feat(msgcenter): /msg 换真实数据——服务端筛选+滚动分页、铃铛直跳、unreadCount 上提 store、WS 通知带应用名
- 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 段
This commit is contained in:
+34
-306
@@ -23,58 +23,14 @@
|
||||
</div>
|
||||
</Transition>
|
||||
</li>
|
||||
<!-- 铃铛直跳 /msg(消息对接 2026-09-30:移除 Poptip 浮层与 900 宽列表 Modal,
|
||||
未读数上提 store 与 /msg 页左栏同源;审批直批弹窗抽至 MsgApprovalModal) -->
|
||||
<li>
|
||||
<Poptip class="fl" popper-class="message-wrap reset-panel" :width="360" trigger="click"
|
||||
placement="bottom-end" v-model="isMsgShow" :transfer="true">
|
||||
<div class="icon-box" v-wave>
|
||||
<Badge :count="unreadCount" :offset="[2, 0]" overflow-count="99">
|
||||
<SvgIcon name="header-2" color="#333333" />
|
||||
</Badge>
|
||||
</div>
|
||||
<template #content>
|
||||
<h3 class="title">查看消息</h3>
|
||||
<vuescroll>
|
||||
<ul class="msg-ul">
|
||||
<li class="no-msg-box" v-if="userMsgList.length == 0">暂无消息</li>
|
||||
<template v-else v-for="(item, index) in userMsgList">
|
||||
<li class="fix">
|
||||
<h4 :class="{
|
||||
'text-ellipsis': true,
|
||||
read: item.isRead === 1,
|
||||
}">
|
||||
<span class="app-name-box">{{ item.appName || msgTypeLabel(item.msgType) }}</span>
|
||||
<span class="content" :title="item.content"
|
||||
@click="onClickMsg(item)">
|
||||
{{ item.title }}:{{ item.content }}
|
||||
</span>
|
||||
</h4>
|
||||
<span v-if="item.msgType === 2" class="approval-box">
|
||||
<a href="javascript:;" class="approval-link approve"
|
||||
@click="openApproval(item, 'approve')">同意</a>
|
||||
<a href="javascript:;" class="approval-link reject"
|
||||
@click="openApproval(item, 'reject')">拒绝</a>
|
||||
</span>
|
||||
<a v-if="item.showJump" href="javascript:;" @click="onClickMsg(item)"
|
||||
title="查看详情">
|
||||
<SvgIcon name="arrow-right" color="#bbbbbb" />
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</vuescroll>
|
||||
<div class="footer fix">
|
||||
<a href="javascript:;" class="fl link-all" @click="showMsgList">查看全部</a>
|
||||
<a href="javascript:;" class="fr read-all" v-if="userMsgList.length > 0"
|
||||
@click="readMsgAll(1)">
|
||||
全部已读
|
||||
</a>
|
||||
|
||||
<!-- <a class="tzql_r" @click="clearMsg" href="javascript:;" title="清空消息"
|
||||
><img src="image/msg-clear.png"
|
||||
/></a> -->
|
||||
</div>
|
||||
</template>
|
||||
</Poptip>
|
||||
<div class="icon-box" v-wave title="消息中心" @click="router.push('/msg')">
|
||||
<Badge :count="unreadCount" :offset="[2, 0]" overflow-count="99">
|
||||
<SvgIcon name="header-2" color="#333333" />
|
||||
</Badge>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<Poptip class="fl" popper-class="roleinfo-wrap reset-panel" :width="240" trigger="click"
|
||||
@@ -175,17 +131,6 @@
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<Modal v-model="isMsgListShow" width="900" :mask="true" title="消息" class-name="msg-modal" :transfer="false">
|
||||
<div class="fix mb-10">
|
||||
<Button type="primary" @click="resetMsgPage" size="small" class="fl ml-10 width-10" ghost>刷新</Button>
|
||||
<Button type="primary" @click="readMsgAll(2)" size="small" class="fl ml-10 width-10" ghost>全部已读</Button>
|
||||
</div>
|
||||
<TablePage :table-height="360" ref="refTablePage" page-type="small" :dataApi="tableApi" :pageParams="pageParams"
|
||||
:columns="columns" :auto-load="false" />
|
||||
<template #footer>
|
||||
<Button type="primary" ghost @click="isMsgListShow = false">关闭</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<Modal v-model="isWallpaperShow" class-name="wallpaper-modal" width="960" :mask="false" title="设置壁纸"
|
||||
:transfer="false">
|
||||
<vuescroll>
|
||||
@@ -277,21 +222,8 @@
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 卡片直批弹窗(B3 §2):同意可选意见 / 拒绝意见必填;身份服务端取会话 -->
|
||||
<Modal v-model="approval.visible" width="460" :mask-closable="false"
|
||||
:title="approval.action === 'approve' ? '同意办理' : '拒绝办理'">
|
||||
<div class="approval-body">
|
||||
<p class="approval-title text-ellipsis">{{ approval.title || "确认办理该待办?" }}</p>
|
||||
<Input v-model="approval.opinion" type="textarea" :rows="3" maxlength="200" show-word-limit
|
||||
:placeholder="approval.action === 'approve' ? '审批意见(可选)' : '请输入拒绝意见(必填)'" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button type="primary" :loading="approval.submitting" @click="submitApproval">
|
||||
{{ approval.action === "approve" ? "确认同意" : "确认拒绝" }}
|
||||
</Button>
|
||||
<Button @click="approval.visible = false">取消</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
<!-- 审批直批弹窗(B3 §2)已抽共享组件(消息对接 2026-09-30),供 /msg 页复用;Header 内已无直批入口 -->
|
||||
<MsgApprovalModal ref="refMsgApproval" />
|
||||
|
||||
<MenuBarModal ref="refMenuBarModal" />
|
||||
<StatsBarModal ref="refStatsBarModal" />
|
||||
@@ -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<any>();
|
||||
const { $conf } = useCurrentInstance();
|
||||
const baseUrl = computed(() => $conf.fileHost);
|
||||
|
||||
/** 未读消息数上提 pinia store(消息对接 2026-09-30):Header 角标与 /msg 页左栏、LeftMenu 同源 */
|
||||
const { unreadCount } = storeToRefs(store);
|
||||
|
||||
const refSearchInput = ref<any>();
|
||||
const refMenuBarModal = ref<any>();
|
||||
const refStatsBarModal = ref<any>();
|
||||
@@ -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<string | null>(null),
|
||||
wallpaperList = ref<any[]>([]),
|
||||
logoList = ref<any[]>([]),
|
||||
userMsgList = ref<any[]>([]),
|
||||
/** 未读消息数(msgCenter/summary 拉取 + WS newMessage 实时累加) */
|
||||
unreadCount = ref(0);
|
||||
logoList = ref<any[]>([]);
|
||||
|
||||
// ===== 消息中心 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<any[]>([
|
||||
{
|
||||
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() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("@/assets/styles/header.scss");
|
||||
|
||||
::v-deep .ivu-table td {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* 卡片直批(B3):待办消息行内「同意/拒绝」入口 */
|
||||
.approval-box {
|
||||
margin-right: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.approval-link {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: #577afb;
|
||||
}
|
||||
|
||||
.approval-link.reject {
|
||||
color: #ed4014;
|
||||
}
|
||||
|
||||
.approval-body {
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.approval-title {
|
||||
margin-bottom: 10px;
|
||||
color: #515a6e;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user