客户超额消息通知

This commit is contained in:
wuweihua
2026-10-09 17:06:40 +08:00
parent 94e7a18e0b
commit 037d4b7cdf
20 changed files with 2065 additions and 7 deletions
@@ -0,0 +1,172 @@
<template>
<el-dialog v-model="notifyState.visible" :title="dialogTitle" width="660px" append-to-body
:close-on-click-modal="false" class="notify-popup-dialog" @open="loadList">
<div class="np-toolbar">
<el-radio-group v-model="readFilter" size="small" @change="loadList">
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="0">未读</el-radio-button>
<el-radio-button value="1">已读</el-radio-button>
</el-radio-group>
<el-button type="primary" link :disabled="!notifyState.unreadCount" @click="handleReadAll">全部标记已读</el-button>
</div>
<el-scrollbar max-height="420px">
<div v-for="item in list" :key="item.receiveId" class="np-item" :class="{ unread: item.readFlag === '0' }">
<div class="np-item-head">
<el-badge is-dot :hidden="item.readFlag === '1'" type="danger">
<span class="np-title">{{ item.title }}</span>
</el-badge>
<span class="np-time">{{ parseTime(item.createTime) }}</span>
</div>
<div class="np-content">{{ item.content }}</div>
<div class="np-foot">
<el-tag v-if="item.eventType === 'CUSTOMER_OVERDUE'" type="danger" size="small">客户已超额</el-tag>
<span class="np-amount" v-if="item.overdueAmount">超额金额:{{ formatMoney(item.overdueAmount) }} CNY</span>
<el-button v-if="item.readFlag === '0'" link type="primary" @click="handleRead(item)">标记已读</el-button>
</div>
</div>
<el-empty v-if="!list.length" :description="readFilter === '0' ? '暂无未读通知' : '暂无通知'" :image-size="80" />
</el-scrollbar>
<template #footer>
<el-button type="primary" @click="notifyState.visible = false">关 闭</el-button>
</template>
</el-dialog>
</template>
<script setup name="NotifyPopup">
import { onMounted, ref, computed } from 'vue'
import { popupNotify, listMyNotify, countMyNotify, readMyNotify, readAllMyNotify } from '@/api/system/notify'
import { notifyState } from './notifyState'
const { proxy } = getCurrentInstance()
const parseTime = proxy.parseTime
const formatMoney = (v) => (v === null || v === undefined || v === '' ? '0.00' : Number(v).toFixed(2))
const list = ref([])
const readFilter = ref('')
const autoOpened = ref(false)
const dialogTitle = computed(() => '我的通知' + (notifyState.unreadCount > 0 ? `(${notifyState.unreadCount} 条未读)` : ''))
function refreshCount() {
countMyNotify().then(res => { notifyState.unreadCount = Number(res.data) || 0 }).catch(() => {})
}
function loadList() {
listMyNotify(readFilter.value).then(res => { list.value = res.data || [] })
}
function handleRead(item) {
readMyNotify(item.receiveId).then(() => {
item.readFlag = '1'
refreshCount()
})
}
function handleReadAll() {
readAllMyNotify().then(() => {
proxy.$modal.msgSuccess('已全部标记为已读')
refreshCount()
loadList()
})
}
/** 对外暴露手动打开入口 */
notifyState.open = () => {
readFilter.value = ''
notifyState.visible = true
}
notifyState.refreshCount = refreshCount
notifyState.loadList = loadList
/**
* 登录后自动弹屏:
* 1) 先让路由/首页渲染稳定;
* 2) 若“初始/过期密码修改”提示正在展示(user.js 写 session 标记),等待其关闭后再弹,避免双框冲突;
* 3) 待弹屏消息只弹一次(后端拉取即置 popup_flag=Y),关闭后可从头像菜单“我的通知”再次查看。
*/
function waitPasswordPromptThenRun(run) {
const START_DELAY = 1200
const POLL_INTERVAL = 800
const MAX_WAIT = 60000
setTimeout(() => {
const begin = Date.now()
const timer = setInterval(() => {
const pwdShowing = sessionStorage.getItem('bmsPwdPrompt') === '1'
if (!pwdShowing) {
clearInterval(timer)
setTimeout(run, 300)
} else if (Date.now() - begin > MAX_WAIT) {
// 用户长时间未处理改密框,超时也弹出通知,避免消息永远看不到
clearInterval(timer)
run()
}
}, POLL_INTERVAL)
}, START_DELAY)
}
onMounted(() => {
refreshCount()
waitPasswordPromptThenRun(() => {
if (autoOpened.value) return
popupNotify().then(res => {
if ((res.data || []).length) {
autoOpened.value = true
readFilter.value = ''
notifyState.visible = true
refreshCount()
}
}).catch(() => {})
})
})
</script>
<style lang="scss" scoped>
.np-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.np-item {
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 10px;
background: var(--el-fill-color-blank);
&.unread {
border-left: 3px solid var(--el-color-danger);
background: var(--el-color-danger-light-9);
}
}
.np-item-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.np-title {
font-weight: 600;
margin-right: 10px;
}
.np-time {
color: #909399;
font-size: 12px;
flex-shrink: 0;
}
.np-content {
color: #606266;
font-size: 13px;
line-height: 1.7;
margin-bottom: 8px;
}
.np-foot {
display: flex;
align-items: center;
gap: 12px;
}
.np-amount {
color: #f56c6c;
font-size: 12px;
font-weight: 600;
}
</style>
@@ -0,0 +1,11 @@
import { reactive } from 'vue'
/**
* 通知弹屏共享状态(单例):
* - 组件挂载于全局 Layout,负责登录后自动拉取待弹屏消息
* - Navbar “我的通知”入口通过 open() 打开同一弹窗
*/
export const notifyState = reactive({
visible: false,
unreadCount: 0
})