This commit is contained in:
wuweihua
2026-10-09 17:20:05 +08:00
20 changed files with 2065 additions and 7 deletions
+110
View File
@@ -0,0 +1,110 @@
import request from '@/utils/request'
// 查询通知规则列表
export function listNotifyRule(query) {
return request({
url: '/system/notify/rule/list',
method: 'get',
params: query
})
}
// 查询通知规则详情
export function getNotifyRule(ruleId) {
return request({
url: '/system/notify/rule/' + ruleId,
method: 'get'
})
}
// 新增通知规则
export function addNotifyRule(data) {
return request({
url: '/system/notify/rule',
method: 'post',
data
})
}
// 修改通知规则
export function updateNotifyRule(data) {
return request({
url: '/system/notify/rule',
method: 'put',
data
})
}
// 删除通知规则
export function delNotifyRule(ruleIds) {
return request({
url: '/system/notify/rule/' + ruleIds,
method: 'delete'
})
}
// 立即执行一次客户超额扫描
export function scanOverdueNow() {
return request({
url: '/system/notify/scan',
method: 'post'
})
}
// 查询通知消息列表
export function listNotifyMessage(query) {
return request({
url: '/system/notify/message/list',
method: 'get',
params: query
})
}
// 查询通知消息详情(含接收人已读情况)
export function getNotifyMessage(messageId) {
return request({
url: '/system/notify/message/' + messageId,
method: 'get'
})
}
// 登录后待弹屏消息(拉取即置弹屏已展示)
export function popupNotify() {
return request({
url: '/system/notify/mine/popup',
method: 'get'
})
}
// 我的通知列表
export function listMyNotify(readFlag) {
return request({
url: '/system/notify/mine/list',
method: 'get',
params: { readFlag }
})
}
// 我的未读数
export function countMyNotify() {
return request({
url: '/system/notify/mine/count',
method: 'get'
})
}
// 标记单条已读
export function readMyNotify(receiveId) {
return request({
url: '/system/notify/mine/read/' + receiveId,
method: 'put'
})
}
// 全部已读
export function readAllMyNotify() {
return request({
url: '/system/notify/mine/readAll',
method: 'put'
})
}
@@ -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
})
@@ -42,6 +42,11 @@
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item command="myNotify">
<el-badge :value="notifyState.unreadCount" :hidden="!notifyState.unreadCount" :max="99" type="danger">
<span>我的通知</span>
</el-badge>
</el-dropdown-item>
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item>
@@ -72,6 +77,7 @@ import useAppStore from '@/store/modules/app'
import useUserStore from '@/store/modules/user'
import useLockStore from '@/store/modules/lock'
import useSettingsStore from '@/store/modules/settings'
import { notifyState } from '@/components/NotifyPopup/notifyState'
// import HeaderNotice from './HeaderNotice' // 消息通知铃铛已隐藏
const route = useRoute()
@@ -93,6 +99,9 @@ function handleCommand(command) {
case "lockScreen":
lockScreen()
break
case "myNotify":
notifyState.open && notifyState.open()
break
case "logout":
logout()
break
+3
View File
@@ -10,6 +10,8 @@
<app-main />
<settings ref="settingRef" />
</div>
<!-- 全局消息通知:登录后未读弹屏 + 头像菜单“我的通知”入口 -->
<notify-popup />
</div>
</template>
@@ -17,6 +19,7 @@
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import NotifyPopup from '@/components/NotifyPopup/index.vue'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
+11 -6
View File
@@ -58,17 +58,22 @@ const useUserStore = defineStore(
this.nickName = user.nickName
this.avatar = avatar
cache.session.set('pwrChrtype', res.pwdChrtype)
/* 改密提示展示标记:通知弹屏组件读取该标记,等改密框关闭后再弹通知,避免双框冲突 */
const showPwdPrompt = (msg) => {
sessionStorage.setItem('bmsPwdPrompt', '1')
ElMessageBox.confirm(msg, '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {}).finally(() => {
sessionStorage.removeItem('bmsPwdPrompt')
})
}
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
showPwdPrompt('您的密码还是初始密码,请修改密码!')
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
showPwdPrompt('您的密码已过期,请尽快修改密码!')
}
resolve(res)
}).catch(error => {
@@ -180,7 +180,7 @@
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="290" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['customersupplier:customerinfo:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:customerinfo:remove']">删除</el-button>
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:customerinfo:remove']">删除</el-button>-->
<el-button link type="primary" icon="Wallet" @click="handleCustomerMoney(scope.row)">客户余额</el-button>
<el-button link type="primary" icon="Document" @click="handleViewLog(scope.row)" v-hasPermi="['customersupplier:customerinfo:query']">日志</el-button>
</template>
+399
View File
@@ -0,0 +1,399 @@
<template>
<div class="app-container">
<el-tabs v-model="activeTab" class="notify-tabs">
<!-- ============ 通知规则 ============ -->
<el-tab-pane label="通知配置" name="rule">
<el-form :model="ruleQuery" ref="ruleQueryRef" :inline="true" v-show="showSearch" label-width="80px">
<el-form-item label="规则名称" prop="ruleName">
<el-input v-model="ruleQuery.ruleName" placeholder="请输入规则名称" clearable style="width: 200px"
@keyup.enter="handleRuleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="ruleQuery.status" placeholder="全部" clearable style="width: 140px">
<el-option v-for="d in sys_normal_disable" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleRuleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetRuleQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAddRule" v-hasPermi="['system:notify:add']">新增配置</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="RefreshRight" :loading="scanLoading"
@click="handleScanNow" v-hasPermi="['system:notify:edit']">立即扫描超额客户</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Refresh" @click="getRuleList">刷新</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getRuleList" />
</el-row>
<el-table v-loading="ruleLoading" :data="ruleList" border>
<el-table-column label="规则名称" prop="ruleName" min-width="180" show-overflow-tooltip />
<el-table-column label="通知事件" align="center" width="150">
<template #default>
<el-tag type="danger" size="small">客户已超额(欠费)</el-tag>
</template>
</el-table-column>
<el-table-column label="通知对象" align="left" min-width="260">
<template #default="scope">
<el-tag v-for="t in scope.row.targets" :key="t.targetType + t.refId"
:type="t.targetType === 'USER' ? 'primary' : 'success'" size="small"
style="margin: 2px;">
{{ t.targetType === 'USER' ? '用户' : '角色' }}:{{ t.refName }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="90">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1"
@change="handleStatusChange(scope.row)" :disabled="!checkPermi(['system:notify:edit'])" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEditRule(scope.row)"
v-hasPermi="['system:notify:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDeleteRule(scope.row)"
v-hasPermi="['system:notify:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="ruleTotal > 0" :total="ruleTotal" v-model:page="ruleQuery.pageNum"
v-model:limit="ruleQuery.pageSize" @pagination="getRuleList" />
</el-tab-pane>
<!-- ============ 通知记录 ============ -->
<el-tab-pane label="通知记录" name="message">
<el-form :model="messageQuery" :inline="true" label-width="90px">
<el-form-item label="客户名称">
<el-input v-model="messageQuery.customerName" placeholder="客户名称" clearable style="width: 180px"
@keyup.enter="getMessageList" />
</el-form-item>
<el-form-item label="标题">
<el-input v-model="messageQuery.title" placeholder="标题关键字" clearable style="width: 180px"
@keyup.enter="getMessageList" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleMessageQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetMessageQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="messageLoading" :data="messageList" border>
<el-table-column label="标题" prop="title" min-width="220" show-overflow-tooltip />
<el-table-column label="客户编号" align="center" prop="customerCode" width="140" show-overflow-tooltip />
<el-table-column label="客户名称" align="center" prop="customerName" width="200" show-overflow-tooltip />
<el-table-column label="超额金额(CNY)" align="center" width="130">
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.overdueAmount) }}</span></template>
</el-table-column>
<el-table-column label="已读/接收" align="center" width="100">
<template #default="scope">
<el-tag :type="scope.row.readCount === scope.row.receiveCount ? 'info' : 'warning'" size="small">
{{ scope.row.readCount }}/{{ scope.row.receiveCount }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="通知时间" align="center" prop="createTime" width="160">
<template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="操作" align="center" width="80">
<template #default="scope">
<el-button link type="primary" @click="handleViewMessage(scope.row)"
v-hasPermi="['system:notify:query']">详情</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="messageTotal > 0" :total="messageTotal" v-model:page="messageQuery.pageNum"
v-model:limit="messageQuery.pageSize" @pagination="getMessageList" />
</el-tab-pane>
</el-tabs>
<!-- ====== 规则新增/修改弹窗 ====== -->
<el-dialog :title="ruleDialog.title" v-model="ruleDialog.visible" width="640px" append-to-body>
<el-form ref="ruleFormRef" :model="ruleDialog.form" :rules="ruleRules" label-width="100px">
<el-form-item label="规则名称" prop="ruleName">
<el-input v-model="ruleDialog.form.ruleName" placeholder="例如:超额通知-财务与销售" maxlength="100" />
</el-form-item>
<el-form-item label="通知事件">
<el-tag type="danger">客户已超额(欠费)</el-tag>
<span class="form-tip">每日 09:00 自动扫描,客户恢复正常后再次超额会再次通知;同一客户每天最多通知一次。</span>
</el-form-item>
<el-form-item label="通知用户" prop="userIds">
<el-select v-model="ruleDialog.form.userIds" multiple filterable collapse-tags collapse-tags-tooltip
placeholder="选择要通知的用户(可多选)" style="width: 100%;">
<el-option v-for="u in userOptions" :key="u.userId"
:label="(u.nickName || u.userName) + '(' + u.userName + ')'" :value="u.userId" />
</el-select>
</el-form-item>
<el-form-item label="通知角色" prop="roleIds">
<el-select v-model="ruleDialog.form.roleIds" multiple filterable collapse-tags collapse-tags-tooltip
placeholder="选择角色后,该角色下所有启用用户都会收到" style="width: 100%;">
<el-option v-for="r in roleOptions" :key="r.roleId" :label="r.roleName" :value="r.roleId" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="ruleDialog.form.status">
<el-radio v-for="d in sys_normal_disable" :key="d.value" :value="d.value">{{ d.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="ruleDialog.form.remark" type="textarea" :rows="2" maxlength="500" />
</el-form-item>
<div class="form-tip" style="margin-left: 100px;">
用户与角色合并去重通知;被停用的用户/角色不能选择。通知本身不涉及任何余额、额度变动。
</div>
</el-form>
<template #footer>
<el-button type="primary" :loading="ruleDialog.loading" @click="submitRule">确 定</el-button>
<el-button @click="ruleDialog.visible = false">取 消</el-button>
</template>
</el-dialog>
<!-- ====== 消息详情弹窗 ====== -->
<el-dialog title="通知详情" v-model="messageDialog.visible" width="680px" append-to-body>
<el-descriptions :column="2" border size="small" v-if="messageDialog.data">
<el-descriptions-item label="标题" :span="2">{{ messageDialog.data.title }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ messageDialog.data.customerName }}({{ messageDialog.data.customerCode }})</el-descriptions-item>
<el-descriptions-item label="超额金额(CNY)">{{ formatMoney(messageDialog.data.overdueAmount) }}</el-descriptions-item>
<el-descriptions-item label="通知时间" :span="2">{{ parseTime(messageDialog.data.createTime) }}</el-descriptions-item>
<el-descriptions-item label="内容" :span="2">{{ messageDialog.data.content }}</el-descriptions-item>
</el-descriptions>
<div class="recv-title">接收情况</div>
<el-table :data="messageDialog.receives" border size="small" max-height="260">
<el-table-column label="接收人" prop="nickName" align="center" width="160">
<template #default="scope">{{ scope.row.nickName || scope.row.userName }}</template>
</el-table-column>
<el-table-column label="账号" prop="userName" align="center" width="160" />
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag :type="scope.row.readFlag === '1' ? 'success' : 'info'" size="small">
{{ scope.row.readFlag === '1' ? '已读' : '未读' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="阅读时间" align="center">
<template #default="scope">{{ scope.row.readTime ? parseTime(scope.row.readTime) : '-' }}</template>
</el-table-column>
</el-table>
<template #footer>
<el-button type="primary" @click="messageDialog.visible = false">关 闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="Notify">
import { optionUserList } from '@/api/system/user'
import request from '@/utils/request'
import {
listNotifyRule, getNotifyRule, addNotifyRule, updateNotifyRule, delNotifyRule,
scanOverdueNow, listNotifyMessage, getNotifyMessage
} from '@/api/system/notify'
const { proxy } = getCurrentInstance()
const { sys_normal_disable } = useDict('sys_normal_disable')
const checkPermi = (keys) => proxy.$auth.hasPermi(keys)
const parseTime = proxy.parseTime
const formatMoney = (v) => (v === null || v === undefined || v === '' ? '0.00' : Number(v).toFixed(2))
const activeTab = ref('rule')
const showSearch = ref(true)
/* ---------- 规则列表 ---------- */
const ruleLoading = ref(false)
const ruleTotal = ref(0)
const ruleList = ref([])
const ruleQuery = reactive({ pageNum: 1, pageSize: 10, ruleName: '', status: '' })
function getRuleList() {
ruleLoading.value = true
listNotifyRule(ruleQuery).then(res => {
ruleList.value = res.rows
ruleTotal.value = res.total
}).finally(() => { ruleLoading.value = false })
}
function handleRuleQuery() { ruleQuery.pageNum = 1; getRuleList() }
function resetRuleQuery() { proxy.resetForm('ruleQueryRef'); handleRuleQuery() }
/* ---------- 规则编辑 ---------- */
const ruleFormRef = ref(null)
const ruleDialog = reactive({ visible: false, title: '', loading: false, form: {} })
const userOptions = ref([])
const roleOptions = ref([])
const ruleRules = {
ruleName: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
userIds: [{ type: 'array', required: false }],
roleIds: [{ type: 'array', required: false }]
}
function loadOptions() {
if (!userOptions.value.length) {
optionUserList().then(res => { userOptions.value = res.data || [] })
}
if (!roleOptions.value.length) {
request({ url: '/system/role/optionselect', method: 'get' })
.then(res => { roleOptions.value = res.data || [] })
}
}
function resetRuleForm() {
ruleDialog.form = {
ruleId: undefined,
ruleName: '',
eventType: 'CUSTOMER_OVERDUE',
status: '0',
remark: '',
userIds: [],
roleIds: [],
targets: []
}
proxy.resetForm('ruleFormRef')
}
function handleAddRule() {
resetRuleForm()
loadOptions()
ruleDialog.title = '新增通知配置'
ruleDialog.visible = true
}
function handleEditRule(row) {
resetRuleForm()
loadOptions()
getNotifyRule(row.ruleId).then(res => {
const d = res.data || {}
ruleDialog.form = {
ruleId: d.ruleId,
ruleName: d.ruleName,
eventType: d.eventType || 'CUSTOMER_OVERDUE',
status: d.status,
remark: d.remark || '',
userIds: (d.targets || []).filter(t => t.targetType === 'USER').map(t => t.refId),
roleIds: (d.targets || []).filter(t => t.targetType === 'ROLE').map(t => t.refId),
targets: d.targets || []
}
ruleDialog.title = '修改通知配置'
ruleDialog.visible = true
})
}
function submitRule() {
ruleFormRef.value.validate(valid => {
if (!valid) return
const f = ruleDialog.form
if (!f.userIds.length && !f.roleIds.length) {
proxy.$modal.msgError('请至少选择一个通知用户或角色')
return
}
const targets = []
f.userIds.forEach(uid => {
const u = userOptions.value.find(x => x.userId === uid)
targets.push({ targetType: 'USER', refId: uid, refName: u ? (u.nickName || u.userName) : String(uid) })
})
f.roleIds.forEach(rid => {
const r = roleOptions.value.find(x => x.roleId === rid)
targets.push({ targetType: 'ROLE', refId: rid, refName: r ? r.roleName : String(rid) })
})
const payload = {
ruleId: f.ruleId,
ruleName: f.ruleName,
eventType: 'CUSTOMER_OVERDUE',
status: f.status,
remark: f.remark,
targets
}
ruleDialog.loading = true
const action = f.ruleId ? updateNotifyRule(payload) : addNotifyRule(payload)
action.then(() => {
proxy.$modal.msgSuccess(f.ruleId ? '修改成功' : '新增成功')
ruleDialog.visible = false
getRuleList()
}).finally(() => { ruleDialog.loading = false })
})
}
function handleStatusChange(row) {
const text = row.status === '0' ? '启用' : '停用'
proxy.$modal.confirm('确认' + text + '规则「' + row.ruleName + '」?').then(() => {
return updateNotifyRule({
ruleId: row.ruleId,
ruleName: row.ruleName,
eventType: row.eventType,
status: row.status,
targets: (row.targets || []).map(t => ({ targetType: t.targetType, refId: t.refId, refName: t.refName }))
})
}).then(() => {
proxy.$modal.msgSuccess(text + '成功')
}).catch(() => {
row.status = row.status === '0' ? '1' : '0'
})
}
function handleDeleteRule(row) {
proxy.$modal.confirm('确认删除规则「' + row.ruleName + '」?删除后不再产生新通知,历史通知记录保留。').then(() => {
return delNotifyRule(row.ruleId)
}).then(() => {
proxy.$modal.msgSuccess('删除成功')
getRuleList()
}).catch(() => {})
}
const scanLoading = ref(false)
function handleScanNow() {
proxy.$modal.confirm('立即扫描所有超额(欠费)客户并按启用配置投递消息?同一客户当天只通知一次。').then(() => {
scanLoading.value = true
return scanOverdueNow()
}).then(res => {
proxy.$modal.msgSuccess(res.msg || '扫描完成')
if (activeTab.value === 'message') getMessageList()
}).catch(() => {}).finally(() => { scanLoading.value = false })
}
/* ---------- 消息记录 ---------- */
const messageLoading = ref(false)
const messageTotal = ref(0)
const messageList = ref([])
const messageQuery = reactive({ pageNum: 1, pageSize: 10, customerName: '', title: '' })
function getMessageList() {
messageLoading.value = true
listNotifyMessage(messageQuery).then(res => {
messageList.value = res.rows
messageTotal.value = res.total
}).finally(() => { messageLoading.value = false })
}
function handleMessageQuery() { messageQuery.pageNum = 1; getMessageList() }
function resetMessageQuery() {
messageQuery.customerName = ''
messageQuery.title = ''
handleMessageQuery()
}
const messageDialog = reactive({ visible: false, data: null, receives: [] })
function handleViewMessage(row) {
getNotifyMessage(row.messageId).then(res => {
messageDialog.data = res.data
messageDialog.receives = (res.data && res.data.receives) || []
messageDialog.visible = true
})
}
getRuleList()
</script>
<style scoped>
.notify-tabs { margin-top: -8px; }
.mb8 { margin-bottom: 12px; }
.money-out { color: #f56c6c; font-weight: 600; }
.form-tip { color: #909399; font-size: 12px; line-height: 1.6; margin-left: 8px; }
.recv-title { font-weight: 600; margin: 14px 0 8px; }
</style>