kuaifan 420d46d5cc feat(apps): 新增应用菜单角标(数字/红点,per-user 实时推送)
插件/微应用可在自己的菜单入口显示数字或红点角标,插件未打开也生效。

- 后端:新增 app_badges 表 + AppBadge 模型 + Module/Badge 业务编排 +
  AppsController(badge__set 应用密钥鉴权 / badge__clear 用户鉴权)
- 每应用独立密钥 APP_SECRET:按 appid 持久化于 appstore config.yml,鉴权校验
- 推送:复用 PushTask 下发 appBadge WS 消息;microapp_menu 附带初始角标
- 前端:appBadges Vuex module + WS 处理 + 三处菜单渲染(应用卡片/主菜单入口/
  父『应用』入口聚合)+ 移动端 Tabbar + 打开即清(badge_clear_on_open)
- 用户离职级联清理;同步 ai-kb 角标知识
2026-06-29 02:32:19 +00:00

216 lines
6.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="mobile-tabbar">
<NetworkException v-if="windowPortrait" type="alert"/>
<ul class="tabbar-box">
<li
v-for="(item, index) in navList"
:key="index"
:class="{active: activeName === item.name}"
@click="toggleRoute(item.name)">
<i class="taskfont" v-html="item.icon"></i>
<div class="tabbar-title">{{ $L(item.label) }}</div>
<template v-if="item.name === 'dashboard'">
<Badge v-if="dashboardTask.overdue_count > 0" class="tabbar-badge" type="error" :overflow-count="999" :count="dashboardTask.overdue_count"/>
<Badge v-else-if="dashboardTask.today_count > 0" class="tabbar-badge" type="info" :overflow-count="999" :count="dashboardTask.today_count"/>
<Badge v-else-if="dashboardTask.todo_count > 0" class="tabbar-badge" type="primary" :overflow-count="999" :count="dashboardTask.todo_count"/>
</template>
<template v-else-if="item.name === 'dialog'">
<Badge class="tabbar-badge" :overflow-count="999" :text="msgUnreadMention"/>
</template>
<template v-else-if="item.name === 'application'">
<Badge v-if="applicationBadgeCount > 0" class="tabbar-badge" :overflow-count="999" :count="applicationBadgeCount"/>
<Badge v-else-if="applicationBadgeDot" class="tabbar-badge" dot/>
</template>
</li>
</ul>
</div>
</template>
<script>
import {mapGetters, mapState} from "vuex";
import NetworkException from "../NetworkException";
import emitter from "../../store/events";
export default {
name: "MobileTabbar",
components: {NetworkException},
data() {
return {
navList: [
{icon: '&#xe6fb;', name: 'dashboard', label: '仪表盘'},
{icon: '&#xe6fa;', name: 'project', label: '项目'},
{icon: '&#xe6eb;', name: 'dialog', label: '消息'},
{icon: '&#xe6b2;', name: 'contacts', label: '通讯录'},
{icon: '&#xe60c;', name: 'application', label: '应用'},
],
};
},
mounted() {
emitter.on('dialogMsgPush', this.updateBadge);
},
beforeDestroy() {
emitter.off('dialogMsgPush', this.updateBadge);
},
computed: {
...mapState(['cacheDialogs']),
...mapGetters(['dashboardTask']),
// 父『应用』入口聚合角标(规则收敛在 appBadges 模块 getter
...mapGetters('appBadges', {
applicationBadgeCount: 'applicationCount',
applicationBadgeDot: 'applicationDot',
}),
/**
* 综合数(未读、提及、待办)
* @returns {string|string}
*/
msgUnreadMention() {
let num = 0; // 未读
let mention = 0; // 提及
this.cacheDialogs.some(dialog => {
num += $A.getDialogUnread(dialog, false);
mention += $A.getDialogMention(dialog);
})
if (num > 999) {
num = "999+"
}
if (mention > 999) {
mention = "999+"
}
const todoNum = this.msgTodoTotal // 待办
if (todoNum) {
if (mention) {
return `@${mention}·${todoNum}`
}
if (num) {
return `${num}·${todoNum}`
}
return todoNum;
}
if (num) {
if (mention) {
return `${num}·@${mention}`
}
return String(num)
}
if (mention) {
return `@${mention}`
}
return "";
},
/**
* 未读消息数
* @returns {number}
*/
msgAllUnread() {
let num = 0;
this.cacheDialogs.some(dialog => {
num += $A.getDialogNum(dialog);
})
return num;
},
/**
* 待办消息数
* @returns {string|null}
*/
msgTodoTotal() {
let todoNum = this.cacheDialogs.reduce((total, current) => total + (current.todo_num || 0), 0)
if (todoNum > 0) {
if (todoNum > 999) {
todoNum = "999+"
} else if (todoNum === 1) {
todoNum = ""
}
return `${this.$L("待办")}${todoNum}`
}
return null;
},
/**
* 未读消息 + 逾期任务
* @returns {number|*}
*/
unreadAndOverdue() {
if (this.userId > 0) {
return this.msgAllUnread + this.dashboardTask.overdue_count
} else {
return 0
}
},
activeName() {
if (['manage-calendar', 'manage-file', 'manage-setting', 'manage-application'].includes(this.routeName)) {
return 'application';
}
if (this.routeName === 'manage-dashboard') {
return 'dashboard';
}
if (this.routeName === 'manage-project' && !/^\d+$/.test(this.$route.params.projectId)) {
return 'project';
}
if (this.routeName === 'manage-messenger') {
if (this.$route.params.dialogAction === 'contacts') {
return 'contacts'
} else {
return 'dialog'
}
}
return ''
},
},
watch: {
windowActive() {
this.updateBadge();
},
},
methods: {
toggleRoute(path) {
this.$emit("on-click", path)
//
let location;
switch (path) {
case 'project':
location = {name: 'manage-project', params: {projectId: 'all'}};
break;
case 'dialog':
location = {name: 'manage-messenger', params: {dialogAction: 'dialog'}};
if (this.routeName === 'manage-messenger') {
emitter.emit('clickAgainDialog', true);
}
break;
case 'contacts':
location = {name: 'manage-messenger', params: {dialogAction: 'contacts'}};
break;
default:
location = {name: 'manage-' + path};
break;
}
this.goForward(location);
},
updateBadge() {
if (this.windowActive) {
return
}
$A.eeuiAppSendMessage({
action: 'setBdageNotify',
bdage: this.unreadAndOverdue,
});
},
},
};
</script>