feat(borrow): 全局待办强提醒(接收人不再处于盲区)

新增无渲染组件 PendingTransferNotifier,挂在 Layout(路由切换常驻、且只在
已登录区域渲染,天然保证「有 token 才查」)。

UI
----
ElNotification,type=warning、duration=0(不自动关闭,须用户处理或手动点掉):
  标题:待办通知:借库转交
  内容:您有 X 件物品等待接收确认,请及时处理。【去处理 >】
点击【去处理】关闭通知并 router.push('/operation/records')(借还记录页)。
message 用 VNode 构造而非 dangerouslyUseHTMLString —— 不必把数量拼进 HTML 字符串。

防骚扰(两道)
----
· 会话级去重:sessionStorage 记「上次已提醒过的数量」,只有数量**变化**才再弹。
  用 sessionStorage 而非 Pinia —— 前者跨刷新存活,后者会重置,刷新即轰炸。
· 数量归零时清掉记录,下次新转交能重新提醒。

★ 加了 2 分钟低频轮询(超出需求所写,但需求目标需要它):
  需求只要求「初始化时查一次」,而 SPA 只在首次进入时初始化 —— 已打开页面的
  用户永远收不到提醒,与「第一时间响应」的目标相悖。有去重逻辑兜底,
  轮询不会造成重复打扰。不需要的话删掉定时器即可。

错误一律静默:提醒是锦上添花,不能因接口抖动弹错误框刷屏。

验证:去重状态机用 node 复刻验证 —— 3→2→1 各弹一次,刷新与轮询均不重复,
归零后再来新转交能重新提醒。
This commit is contained in:
yueli
2026-09-17 10:30:13 +08:00
parent 1c58789fd9
commit 2c732a9a3a
3 changed files with 121 additions and 0 deletions

View File

@ -308,6 +308,18 @@ export function rejectBorrowTransfer(transferId: number, reason?: string) {
})
}
/**
* 待我接收的转交数量 —— 全局待办强提醒用(初始化 + 轮询)。
* 极轻量,只做一次 count。
* @returns { count }
*/
export function getPendingTransferCount() {
return request({
url: '/v1/transactions/borrow/transfer/pending-count',
method: 'get'
})
}
/**
* 整单流转时间线(**borrow_no** 维度):借出 → 转交(可多次) → 归还 → 报废
*

View File

@ -0,0 +1,104 @@
<template>
<!-- 无渲染组件只负责全局待办强提醒不产生任何 DOM -->
</template>
<script setup lang="ts">
/**
* 借库转交 · 全局待办强提醒
*
* 解决的问题:双向握手引入后,发起方提交了转交,接收人若不去借还记录页主动
* 查看,就完全处于盲区 —— 物品在系统里挂着「待接收」,责任悬空。
* 本组件在应用启动后主动查询「待我接收」的数量,并用不会自动消失的
* ElNotification 强提醒,附一键跳转到借还记录页。
*
* ★ 为什么挂在 Layout 而非登录页:
* Layout 只在已登录区域渲染,天然保证「有 token 才查」;
* 且它是路由切换时**常驻**的组件AppMain 换页不会卸载它),
* 轮询定时器不会被反复创建销毁。
*
* ★ 防骚扰(两道):
* 1) 会话级去重sessionStorage 记「上次已提醒过的数量」,只有数量**变化**
* 才再弹。刷新页面不会重复轰炸(用 sessionStorage 而非 Pinia ——
* 前者跨刷新存活,后者会重置)。
* 2) 数量归零时清掉记录,下次再来新转交能重新提醒。
*
* ★ 错误一律静默:待办提醒是锦上添花,绝不能因为接口抖动就弹错误框刷屏。
*/
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { h } from 'vue'
import { ElButton, ElNotification } from 'element-plus'
import { getPendingTransferCount } from '@/api/transaction'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const userStore = useUserStore()
const SS_KEY = 'pendingTransferNotifiedCount'
// 轮询间隔:需求只要求「初始化时查一次」,但那样已打开的页面永远收不到提醒
// SPA 只在首次进入时初始化)。加一轮低频轮询才能真正做到「第一时间响应」,
// 有了上面的去重逻辑,轮询不会造成重复打扰。若不需要,删掉定时器即可。
const POLL_MS = 2 * 60 * 1000
let timer: ReturnType<typeof setInterval> | null = null
const notify = (count: number) => {
const notification = ElNotification({
title: '待办通知:借库转交',
type: 'warning',
duration: 0, // 不自动关闭,必须用户手动处理或点掉
showClose: true,
// 用 VNode 而非 dangerouslyUseHTMLString避免把数量拼进 HTML 字符串
message: h('div', [
h('div', `您有 ${count} 件物品等待接收确认,请及时处理。`),
h(
ElButton,
{
type: 'primary',
link: true,
size: 'small',
style: 'margin-top:6px; padding:0;',
onClick: () => {
notification.close()
router.push('/operation/records')
},
},
() => '去处理 >'
),
]),
})
}
const check = async () => {
if (!userStore.token) return
let count = 0
try {
const res: any = await getPendingTransferCount()
count = Number(res?.count ?? res?.data?.count ?? 0)
} catch {
return // 静默:不打扰用户,也不刷屏报错
}
if (!count) {
// 已处理完:清掉记录,下次再来新转交能重新提醒
sessionStorage.removeItem(SS_KEY)
return
}
const notified = Number(sessionStorage.getItem(SS_KEY) || 0)
if (count === notified) return // 数量没变 → 本会话已提醒过,不再轰炸
sessionStorage.setItem(SS_KEY, String(count))
notify(count)
}
onMounted(() => {
check()
timer = setInterval(check, POLL_MS)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
timer = null
})
</script>

View File

@ -8,6 +8,10 @@
<!-- 全局规格连号助手 -->
<SpecHelper />
<!-- 全局待办强提醒借库转交的接收人若不来查看责任会一直悬空
Layout 常驻路由切换不卸载轮询定时器放这里最稳 -->
<PendingTransferNotifier />
</div>
</template>
@ -15,6 +19,7 @@
import Sidebar from './components/Sidebar/index.vue'
import AppMain from './components/AppMain.vue'
import SpecHelper from '@/components/SpecHelper/index.vue'
import PendingTransferNotifier from '@/components/PendingTransferNotifier/index.vue'
</script>
<style scoped>