feat(msg): 计数口径改消息总数 + 标识切换 appId
build / build (push) Successful in 43s

- 按分类计数改分类总数(sysTotal/bizTotal/approvalTotal,与列表内容对齐),已读=已读总数、全部=全部总数
- 按应用角标改 countByApp 按应用消息总数(含已读,已读后不清零),总数>0 才显示、总数降序
- 应用匹配/筛选全面改 appId:行与计数出参带 appId、筛选传 appId,appKey 不再出现在消息中心前端
This commit is contained in:
chenxing
2026-10-08 16:37:22 +08:00
parent 3250de382a
commit f9f97db272
2 changed files with 49 additions and 44 deletions
+13 -9
View File
@@ -107,18 +107,18 @@ class Base extends Abstract {
// ===== 消息中心代理(§3.5:经后端签名代理 UserMsg,前端不直连)===== // ===== 消息中心代理(§3.5:经后端签名代理 UserMsg,前端不直连)=====
/** /**
* 消息中心分页列表({total, list[]};元素含改写后的绝对 jumpUrl)。 * 消息中心分页列表({total, list[]};元素含改写后的绝对 jumpUrl、appId/appName)。
* 可选服务端筛选:msgType 0/1/2、appKey、isRead 0/1、keyword 标题/内容模糊。 * 可选服务端筛选:msgType 0/1/2、appId(注册表应用 id)、isRead 0/1、keyword 标题/内容模糊。
* @memberof Basic * @memberof Basic
*/ */
getMsgCenterList(params: PageParams & { msgType?: number; appKey?: string; isRead?: number; keyword?: string }) { getMsgCenterList(params: PageParams & { msgType?: number; appId?: string; isRead?: number; keyword?: string }) {
const url = path + "/msgCenter/list"; const url = path + "/msgCenter/list";
return this.getRequest<PageData>({ url, params }); return this.getRequest<PageData>({ url, params });
} }
/** /**
* 铃铛摘要:未读数 + 各应用待办角标 + 分类计数(sysUnread/bizUnread/approvalUnread/total/readCount, * 铃铛摘要:未读数 + 各应用待办角标 + 分类计数(sysUnread/bizUnread/approvalUnread 为未读口径,
* 旧版 UserMsg 无 summary 端点时这些字段缺省,前端按降级处理) * sysTotal/bizTotal/approvalTotal/total/readCount 为总数口径;旧版 UserMsg 无这些端点字段时缺省,前端按降级处理)
* @memberof Basic * @memberof Basic
*/ */
getMsgCenterSummary() { getMsgCenterSummary() {
@@ -129,18 +129,22 @@ class Base extends Abstract {
sysUnread?: number; sysUnread?: number;
bizUnread?: number; bizUnread?: number;
approvalUnread?: number; approvalUnread?: number;
sysTotal?: number;
bizTotal?: number;
approvalTotal?: number;
total?: number; total?: number;
readCount?: number; readCount?: number;
}>({ url }); }>({ url });
} }
/** /**
* 消息中心:按来源应用未读数映射(「按应用」清单角标):{appKey: 未读数},无未读的 appKey 不出现 * 消息中心:按来源应用消息计数(「按应用」清单角标):[{appId, appName, total, unread}],
* total 含已读;无消息的应用不出现
* @memberof Basic * @memberof Basic
*/ */
getMsgCenterUnreadCountByApp() { getMsgCenterCountByApp() {
const url = path + "/msgCenter/unreadCountByApp"; const url = path + "/msgCenter/countByApp";
return this.getRequest<Record<string, number>>({ url }); return this.getRequest<{ appId: number | null; appName: string | null; total: number; unread: number }[]>({ url });
} }
/** /**
+36 -35
View File
@@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<!-- 模式二:按应用(应用清单=GetUserApp 用户应用全集;角标=unreadCountByApp 按应用未读数) --> <!-- 模式二:按应用(应用清单=GetUserApp 用户应用全集;角标=countByApp 消息总数,0 不显示) -->
<div v-show="mode === 'app'"> <div v-show="mode === 'app'">
<div class="side-title">全部应用</div> <div class="side-title">全部应用</div>
<div :class="['cat', { active: curApp === 'all' }]" @click="onClickApp('all')"> <div :class="['cat', { active: curApp === 'all' }]" @click="onClickApp('all')">
@@ -32,9 +32,9 @@
<div class="side-sep"></div> <div class="side-sep"></div>
<div <div
v-for="a in appList" v-for="a in appList"
:key="a.appKey" :key="a.appId"
:class="['cat', { active: curApp === a.appKey }]" :class="['cat', { active: curApp === String(a.appId) }]"
@click="onClickApp(a.appKey)" @click="onClickApp(String(a.appId))"
> >
<span class="dot dot-primary"></span>{{ a.name }}<span class="cnt">{{ a.cnt }}</span> <span class="dot dot-primary"></span>{{ a.name }}<span class="cnt">{{ a.cnt }}</span>
</div> </div>
@@ -129,7 +129,7 @@ interface MsgRow {
content: string; content: string;
msgType: number; // 0 系统通知 / 1 业务提醒 / 2 审批待办 msgType: number; // 0 系统通知 / 1 业务提醒 / 2 审批待办
appName?: string | null; appName?: string | null;
appKey?: string | null; appId?: number | null; // 注册表应用 id(appKey 不下发前端)
sendTime?: string; // yyyy-MM-dd HH:mm:ss sendTime?: string; // yyyy-MM-dd HH:mm:ss
isRead: number; // 1=已读 isRead: number; // 1=已读
jumpUrl?: string | null; jumpUrl?: string | null;
@@ -158,39 +158,40 @@ const loadError = ref(false);
const hasMore = computed(() => rows.value.length < total.value); const hasMore = computed(() => rows.value.length < total.value);
const refMsgBody = ref<HTMLElement | null>(null); const refMsgBody = ref<HTMLElement | null>(null);
/** summary 左栏计数(旧版 UserMsg 无 summary 端点时分类字段缺省 → 显示 —) */ /** summary 左栏计数(分类总数口径;旧版 UserMsg 无字段时缺省 → 显示 —) */
const catCounts = ref<{ sysUnread?: number; bizUnread?: number; approvalUnread?: number; total?: number; readCount?: number }>({}); const catCounts = ref<{ sysTotal?: number; bizTotal?: number; approvalTotal?: number; total?: number; readCount?: number }>({});
const summaryTotalHint = computed(() => (catCounts.value.total == null ? "—" : String(catCounts.value.total))); const summaryTotalHint = computed(() => (catCounts.value.total == null ? "—" : String(catCounts.value.total)));
function isUnread(m: MsgRow) { function isUnread(m: MsgRow) {
return m.isRead !== 1; return m.isRead !== 1;
} }
/** appName 缺失降级:按 appKey 匹配用户应用清单(与桌面网格同源),再退类型标签 */ /** appName 缺失降级:按 appId 匹配用户应用清单(与桌面网格同源),再退类型标签 */
function typeAppName(m: MsgRow) { function typeAppName(m: MsgRow) {
if (m.appKey) { if (m.appId != null) {
const hit = userApps.value.find(a => a.appKey === String(m.appKey)); const hit = userApps.value.find(a => a.appId === m.appId);
if (hit && hit.name) return hit.name; if (hit && hit.name) return hit.name;
} }
return typeName(m.msgType); return typeName(m.msgType);
} }
/** 分类导航(计数走 summary:分类未读数;「已读」= 已读总数) */ /** 分类导航(计数=分类消息总数含已读,与列表内容对齐;「全部」=全部总数、「已读」=已读总数) */
const catList = computed(() => [ const catList = computed(() => [
{ key: "all", name: "全部消息", color: "#5979ea", cnt: catCounts.value.total == null ? "—" : catCounts.value.total }, { key: "all", name: "全部消息", color: "#5979ea", cnt: catCounts.value.total == null ? "—" : catCounts.value.total },
{ key: "sys", name: "系统通知", color: "#5979ea", cnt: catCounts.value.sysUnread == null ? "—" : catCounts.value.sysUnread }, { key: "sys", name: "系统通知", color: "#5979ea", cnt: catCounts.value.sysTotal == null ? "—" : catCounts.value.sysTotal },
{ key: "biz", name: "业务提醒", color: "#e9b954", cnt: catCounts.value.bizUnread == null ? "—" : catCounts.value.bizUnread }, { key: "biz", name: "业务提醒", color: "#e9b954", cnt: catCounts.value.bizTotal == null ? "—" : catCounts.value.bizTotal },
{ key: "appr", name: "审批待办", color: "#e8684a", cnt: catCounts.value.approvalUnread == null ? "—" : catCounts.value.approvalUnread }, { key: "appr", name: "审批待办", color: "#e8684a", cnt: catCounts.value.approvalTotal == null ? "—" : catCounts.value.approvalTotal },
{ key: "read", name: "已读", color: "#c0c6ce", cnt: catCounts.value.readCount == null ? "—" : catCounts.value.readCount }, { key: "read", name: "已读", color: "#c0c6ce", cnt: catCounts.value.readCount == null ? "—" : catCounts.value.readCount },
]); ]);
/** 应用导航:清单=用户应用全集(GetUserApp,BoaApp 自带 appKey/name,与桌面网格同源);角标=按应用未读数 */ /** 应用导航:清单=用户应用全集(GetUserApp);角标=countByApp 按应用消息总数(含已读),0 不显示 */
const userApps = ref<{ appKey: string; name: string }[]>([]); const userApps = ref<{ appId: number; name: string }[]>([]);
const unreadByApp = ref<Record<string, number>>({}); const appCountByAppId = ref<Record<number, { total: number; unread: number }>>({});
const appList = computed(() => const appList = computed(() =>
userApps.value userApps.value
.map(a => ({ appKey: a.appKey, name: a.name, cnt: unreadByApp.value[a.appKey] || 0 })) .map(a => ({ appId: a.appId, name: a.name, cnt: appCountByAppId.value[a.appId]?.total || 0 }))
.sort((x, y) => y.cnt - x.cnt) // 未读多的排前(稳定排序,同计数保持注册表顺序) .filter(a => a.cnt > 0)
.sort((x, y) => y.cnt - x.cnt) // 总数多的排前(稳定排序,同计数保持注册表顺序)
); );
/** 应用清单一次拉齐(GetUserApp;失败降级空,不打扰列表) */ /** 应用清单一次拉齐(GetUserApp;失败降级空,不打扰列表) */
@@ -199,8 +200,8 @@ async function loadApps() {
const groups: any[] = await BaseApi.getUserApp(store.getCurUserId, store.getCurSchId); const groups: any[] = await BaseApi.getUserApp(store.getCurUserId, store.getCurSchId);
userApps.value = (groups || []) userApps.value = (groups || [])
.flatMap((g: any) => (g && Array.isArray(g.apps) ? g.apps : [])) .flatMap((g: any) => (g && Array.isArray(g.apps) ? g.apps : []))
.filter((a: any) => a && a.appKey != null) .filter((a: any) => a && a.id != null)
.map((a: any) => ({ appKey: String(a.appKey), name: a.name })); .map((a: any) => ({ appId: Number(a.id), name: a.name }));
} catch (e) { } catch (e) {
/* 应用清单失败降级空 */ /* 应用清单失败降级空 */
} }
@@ -213,13 +214,13 @@ const curTitle = computed(() => {
return hit ? hit.name : "全部消息"; return hit ? hit.name : "全部消息";
} }
if (curApp.value === "all") return "全部应用消息"; if (curApp.value === "all") return "全部应用消息";
const hit = appList.value.find(a => a.appKey === curApp.value); const hit = appList.value.find(a => String(a.appId) === curApp.value);
return hit ? hit.name : "全部应用消息"; return hit ? hit.name : "全部应用消息";
}); });
/** 当前筛选条件 → 服务端参数(筛选语义归服务端,本地不过滤) */ /** 当前筛选条件 → 服务端参数(筛选语义归服务端,本地不过滤;应用筛选传注册表 appId) */
function currentParams(extra: { page: number }) { function currentParams(extra: { page: number }) {
const params: { page: number; count: number; msgType?: number; appKey?: string; isRead?: number; keyword?: string } = { const params: { page: number; count: number; msgType?: number; appId?: string; isRead?: number; keyword?: string } = {
page: extra.page, page: extra.page,
count: PAGE_SIZE, count: PAGE_SIZE,
}; };
@@ -229,7 +230,7 @@ function currentParams(extra: { page: number }) {
else if (curCat.value === "appr") params.msgType = 2; else if (curCat.value === "appr") params.msgType = 2;
else if (curCat.value === "read") params.isRead = 1; else if (curCat.value === "read") params.isRead = 1;
} else if (curApp.value !== "all") { } else if (curApp.value !== "all") {
params.appKey = curApp.value; params.appId = curApp.value;
} }
const kw = searchText.value.trim(); const kw = searchText.value.trim();
if (kw) params.keyword = kw; if (kw) params.keyword = kw;
@@ -270,28 +271,28 @@ function onScroll() {
} }
} }
/** 左栏计数一次拉齐(未读总数进 store 与铃铛同源;按应用未读数一并拉取) */ /** 左栏计数一次拉齐(未读总数进 store 与铃铛同源;按应用消息计数一并拉取) */
async function loadSummary() { async function loadSummary() {
try { try {
const [s, byApp] = await Promise.all([ const [s, byApp] = await Promise.all([
BaseApi.getMsgCenterSummary(), BaseApi.getMsgCenterSummary(),
BaseApi.getMsgCenterUnreadCountByApp().catch(() => null), BaseApi.getMsgCenterCountByApp().catch(() => null),
]); ]);
if (s) { if (s) {
store.setUnreadCount(s.unreadCount); store.setUnreadCount(s.unreadCount);
catCounts.value = { catCounts.value = {
sysUnread: (s as any).sysUnread, sysTotal: (s as any).sysTotal,
bizUnread: (s as any).bizUnread, bizTotal: (s as any).bizTotal,
approvalUnread: (s as any).approvalUnread, approvalTotal: (s as any).approvalTotal,
total: (s as any).total, total: (s as any).total,
readCount: (s as any).readCount, readCount: (s as any).readCount,
}; };
} }
const map: Record<string, number> = {}; const map: Record<number, { total: number; unread: number }> = {};
if (byApp && typeof byApp === "object") { ((byApp as any) || []).forEach((e: any) => {
Object.entries(byApp).forEach(([k, v]) => (map[k] = Number(v) || 0)); if (e && e.appId != null) map[Number(e.appId)] = { total: Number(e.total) || 0, unread: Number(e.unread) || 0 };
} });
unreadByApp.value = map; appCountByAppId.value = map;
} catch (e) { } catch (e) {
/* 摘要失败不打扰列表 */ /* 摘要失败不打扰列表 */
} }