Files
crawler-plugin/frontend-vue/src/shared/utils/notification-bell.ts
T
huangzd1997 b70557a077 feat(认证/通知): 单设备登录互踢 + 站内通知铃铛系统
- 单设备登录:登录成功即 last-login-wins 绑定 users.machine;非超管旧 token
  在下一次受保护请求抛 4011 下线,超管豁免;仅认 token 内签名 deviceId,不用请求头。
  前端两端接入踢下线跳转(?kicked=1 提示),V118 清空历史 machine
- 站内通知:新增 notification 模块(任务失败扫描 / 密钥欠费 / 下游服务探测三类来源),
  前后台铃铛组件 + 轮询;后台列表按主管数据范围(UserDataScopeSupport)过滤;V116 建表

均已于 2026-09-13 部署上线,此次补提交源码(此前仅存在于已部署 JAR/构建产物中)
2026-09-13 23:08:22 +08:00

98 lines
3.1 KiB
TypeScript

/**
* 铃铛通知纯逻辑:未读徽标文案、新通知判定、「已提醒过」去重记录、时间展示。
* 轮询调度与渲染在 NotificationBell.vue;这里只放可单测的纯函数与存储读写。
*/
/** 未读轮询间隔(60 秒)。 */
export const NOTIFICATION_POLL_INTERVAL_MS = 60_000
const NOTIFIED_KEY_PREFIX = 'notification:last-notified-id'
/** 未读徽标文案:0 或非法值显示空串,>99 显示 99+。 */
export function formatUnreadBadge(count: number | null | undefined): string {
const value = Number(count ?? 0)
if (!Number.isFinite(value) || value <= 0) {
return ''
}
return value > 99 ? '99+' : String(Math.floor(value))
}
/** 是否出现新通知:latestId 大于上次已提醒过的 id。 */
export function hasNewNotification(
latestId: number | null | undefined,
lastNotifiedId: number | null | undefined,
): boolean {
const latest = Number(latestId ?? 0)
const notified = Number(lastNotifiedId ?? 0)
if (!Number.isFinite(latest) || latest <= 0) {
return false
}
return latest > (Number.isFinite(notified) && notified > 0 ? notified : 0)
}
function storageKey(uid: string): string {
return `${NOTIFIED_KEY_PREFIX}:${uid || '0'}`
}
/** 读取当前用户「已提醒过的最大通知 id」(多页面/多会话共享)。 */
export function readLastNotifiedId(uid: string): number {
try {
const raw = window.localStorage.getItem(storageKey(uid))
const value = Number(raw ?? 0)
return Number.isFinite(value) && value > 0 ? value : 0
} catch {
return 0
}
}
export function writeLastNotifiedId(uid: string, id: number): void {
try {
if (Number.isFinite(id) && id > 0) {
window.localStorage.setItem(storageKey(uid), String(Math.floor(id)))
}
} catch {
/* 存储不可用时静默忽略(退化为每次都提醒,不影响功能) */
}
}
/** 当前登录用户 uid(未登录返回 '0')。 */
export function currentNotificationUid(): string {
try {
return window.localStorage.getItem('uid') || '0'
} catch {
return '0'
}
}
/** 通知时间展示:今天只显示 HH:mm;昨天显示「昨天 HH:mm」;更早显示 MM-DD HH:mm。 */
export function formatNotificationTime(
value: string | null | undefined,
now: Date = new Date(),
): string {
if (!value) {
return ''
}
const date = new Date(value)
if (Number.isNaN(date.getTime())) {
return ''
}
const pad = (input: number) => String(input).padStart(2, '0')
const hourMinute = `${pad(date.getHours())}:${pad(date.getMinutes())}`
const sameDay =
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate()
if (sameDay) {
return hourMinute
}
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
const isYesterday =
date.getFullYear() === yesterday.getFullYear() &&
date.getMonth() === yesterday.getMonth() &&
date.getDate() === yesterday.getDate()
if (isYesterday) {
return `昨天 ${hourMinute}`
}
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${hourMinute}`
}