Merge branch 'master' of https://git.lionquexpress.com/wuweihua/BMS
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user