This commit is contained in:
wuweihua
2026-10-09 17:20:05 +08:00
20 changed files with 2065 additions and 7 deletions
@@ -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>