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:
@ -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** 维度):借出 → 转交(可多次) → 归还 → 报废
|
* 整单流转时间线(**borrow_no** 维度):借出 → 转交(可多次) → 归还 → 报废
|
||||||
*
|
*
|
||||||
|
|||||||
104
inventory-web/src/components/PendingTransferNotifier/index.vue
Normal file
104
inventory-web/src/components/PendingTransferNotifier/index.vue
Normal 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>
|
||||||
@ -8,6 +8,10 @@
|
|||||||
|
|
||||||
<!-- 全局规格连号助手 -->
|
<!-- 全局规格连号助手 -->
|
||||||
<SpecHelper />
|
<SpecHelper />
|
||||||
|
|
||||||
|
<!-- 全局待办强提醒:借库转交的接收人若不来查看,责任会一直悬空。
|
||||||
|
Layout 常驻(路由切换不卸载),轮询定时器放这里最稳。 -->
|
||||||
|
<PendingTransferNotifier />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -15,6 +19,7 @@
|
|||||||
import Sidebar from './components/Sidebar/index.vue'
|
import Sidebar from './components/Sidebar/index.vue'
|
||||||
import AppMain from './components/AppMain.vue'
|
import AppMain from './components/AppMain.vue'
|
||||||
import SpecHelper from '@/components/SpecHelper/index.vue'
|
import SpecHelper from '@/components/SpecHelper/index.vue'
|
||||||
|
import PendingTransferNotifier from '@/components/PendingTransferNotifier/index.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user