源代码续
/**
* 通知工具类
*/
// 通知类型常量
export const NotificationType = {
SYSTEM: 1,
EXPRESS: 2,
ACTIVITY: 3
}
// 通知类型名称映射
export const NotificationTypeNames = {
[NotificationType.SYSTEM]: '系统通知',
[NotificationType.EXPRESS]: '快递通知',
[NotificationType.ACTIVITY]: '活动通知'
}
// 通知渠道常量
export const NotificationChannel = {
IN_APP: 1,
SMS: 2,
EMAIL: 3,
PUSH: 4
}
// 通知渠道名称映射
export const NotificationChannelNames = {
[NotificationChannel.IN_APP]: '站内信',
[NotificationChannel.SMS]: '短信',
[NotificationChannel.EMAIL]: '邮件',
[NotificationChannel.PUSH]: '推送'
}
// 通知状态常量
export const NotificationStatus = {
DRAFT: 0,
SENT: 1,
FAILED: 2
}
// 通知状态名称映射
export const NotificationStatusNames = {
[NotificationStatus.DRAFT]: '草稿',
[NotificationStatus.SENT]: '已发送',
[NotificationStatus.FAILED]: '发送失败'
}
/**
* 获取通知类型对应的标签类型
* @param {number} type 通知类型
* @returns {string} 标签类型
*/
export function getTypeTagType(type) {
switch (type) {
case NotificationType.SYSTEM:
return 'primary'
case NotificationType.EXPRESS:
return 'success'
case NotificationType.ACTIVITY:
return 'warning'
default:
return 'info'
}
}
/**
* 获取通知类型对应的颜色
* @param {number} type 通知类型
* @returns {string} 颜色
*/
export function getTypeColor(type) {
switch (type) {
case NotificationType.SYSTEM:
return '#409EFF'
case NotificationType.EXPRESS:
return '#67C23A'
case NotificationType.ACTIVITY:
return '#E6A23C'
default:
return '#909399'
}
}
/**
* 获取通知渠道对应的标签类型
* @param {number} channel 通知渠道
* @returns {string} 标签类型
*/
export function getChannelTagType(channel) {
switch (channel) {
case NotificationChannel.IN_APP:
return 'primary'
case NotificationChannel.SMS:
return 'success'
case NotificationChannel.EMAIL:
return 'warning'
case NotificationChannel.PUSH:
return 'danger'
default:
return 'info'
}
}
/**
* 获取通知状态对应的标签类型
* @param {number} status 通知状态
* @returns {string} 标签类型
*/
export function getStatusTagType(status) {
switch (status) {
case NotificationStatus.DRAFT:
return 'info'
case NotificationStatus.SENT:
return 'success'
case NotificationStatus.FAILED:
return 'danger'
default:
return 'info'
}
}
/**
* 提取模板变量
* @param {string} content 模板内容
* @returns {Array} 变量数组
*/
export function extractTemplateVariables(content) {
if (!content) return []
const regex = /\{\{([^}]+)\}\}/g
let match
const variables = []
while ((match = regex.exec(content)) !== null) {
variables.push({
name: match[1].trim(),
placeholder: match[0],
value: ''
})
}
// 去重
return variables.filter((v, i, a) => a.findIndex(t => t.name === v.name) === i)
}
/**
* 替换模板变量
* @param {string} content 模板内容
* @param {Array} variables 变量数组
* @returns {string} 替换后的内容
*/
export function replaceTemplateVariables(content, variables) {
if (!content || !variables || variables.length === 0) return content
let result = content
variables.forEach(variable => {
if (variable.value) {
const regex = new RegExp(variable.placeholder, 'g')
result = result.replace(regex, variable.value)
}
})
return result
}
/**
* 格式化文件大小
* @param {number} size 文件大小(字节)
* @returns {string} 格式化后的文件大小
*/
export function formatFileSize(size) {
if (size < 1024) {
return size + ' B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB'
} else if (size < 1024 * 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(2) + ' MB'
} else {
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
}
}
/**
* 格式化通知内容(将换行符转换为HTML换行)
* @param {string} content 通知内容
* @returns {string} 格式化后的内容
*/
export function formatContent(content) {
if (!content) return ''
return content.replace(/\n/g, '<br>')
}
/**
* 获取未读通知数量
* @param {Array} notifications 通知列表
* @returns {number} 未读通知数量
*/
export function getUnreadCount(notifications) {
if (!notifications || notifications.length === 0) return 0
return notifications.filter(item => !item.read).length
}
/**
* 获取指定类型的通知数量
* @param {Array} notifications 通知列表
* @param {number} type 通知类型
* @returns {number} 指定类型的通知数量
*/
export function getTypeCount(notifications, type) {
if (!notifications || notifications.length === 0) return 0
return notifications.filter(item => item.type === type).length
}
/**
* 获取指定渠道的通知数量
* @param {Array} notifications 通知列表
* @param {number} channel 通知渠道
* @returns {number} 指定渠道的通知数量
*/
export function getChannelCount(notifications, channel) {
if (!notifications || notifications.length === 0) return 0
return notifications.filter(item => item.channel === channel).length
}
express-ui\src\views\notification\dashboard.vue
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 统计卡片 -->
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6" v-for="(item, index) in statisticsCards" :key="index">
<el-card class="stat-card" :body-style="{ padding: '20px' }" shadow="hover">
<div class="card-icon">
<i :class="item.icon" :style="{ color: item.color }"></i>
</div>
<div class="card-content">
<div class="card-title">{{ item.title }}</div>
<div class="card-value">{{ item.value }}</div>
<div class="card-footer">
<span>{{ item.change >= 0 ? '+' : '' }}{{ item.change }}%</span>
<span>较上周</span>
<i :class="item.change >= 0 ? 'el-icon-top' : 'el-icon-bottom'"
:style="{ color: item.change >= 0 ? '#67C23A' : '#F56C6C' }"></i>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 通知发送趋势图 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>通知发送趋势</span>
<el-radio-group v-model="trendTimeRange" size="mini" style="float: right;">
<el-radio-button label="week">本周</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="year">全年</el-radio-button>
</el-radio-group>
</div>
<div class="chart-container">
<div ref="trendChart" style="width: 100%; height: 300px;"></div>
</div>
</el-card>
</el-col>
<!-- 通知渠道分布图 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>通知渠道分布</span>
<el-radio-group v-model="channelTimeRange" size="mini" style="float: right;">
<el-radio-button label="week">本周</el-radio-button>
<el-radio-button label="month">本月</el-radio-button>
<el-radio-button label="year">全年</el-radio-button>
</el-radio-group>
</div>
<div class="chart-container">
<div ref="channelChart" style="width: 100%; height: 300px;"></div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 最近发送的通知 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>最近发送的通知</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="viewMore('list')">查看更多</el-button>
</div>
<el-table :data="recentNotifications" style="width: 100%" :show-header="false">
<el-table-column width="50">
<template slot-scope="scope">
<el-avatar :size="30" :style="{ backgroundColor: getTypeColor(scope.row.type) }">
{{ scope.row.title.substring(0, 1) }}
</el-avatar>
</template>
</el-table-column>
<el-table-column>
<template slot-scope="scope">
<div class="notification-item">
<div class="notification-title">
<span>{{ scope.row.title }}</span>
<el-tag size="mini" :type="getTypeTag(scope.row.type)">{{ getTypeName(scope.row.type) }}</el-tag>
</div>
<div class="notification-content">{{ scope.row.content }}</div>
<div class="notification-footer">
<span>{{ scope.row.sendTime }}</span>
<span>
<el-tag size="mini" :type="getChannelTag(scope.row.channel)">
{{ getChannelName(scope.row.channel) }}
</el-tag>
</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="viewDetail(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<!-- 最近更新的模板 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>最近更新的模板</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="viewMore('template')">查看更多</el-button>
</div>
<el-table :data="recentTemplates" style="width: 100%" :show-header="false">
<el-table-column width="50">
<template slot-scope="scope">
<el-avatar :size="30" :style="{ backgroundColor: getTypeColor(scope.row.type) }">
{{ scope.row.name.substring(0, 1) }}
</el-avatar>
</template>
</el-table-column>
<el-table-column>
<template slot-scope="scope">
<div class="template-item">
<div class="template-title">
<span>{{ scope.row.name }}</span>
<el-tag size="mini" :type="getTypeTag(scope.row.type)">{{ getTypeName(scope.row.type) }}</el-tag>
</div>
<div class="template-content">{{ scope.row.content }}</div>
<div class="template-footer">
<span>{{ scope.row.updateTime }}</span>
<span>
<el-tag v-for="channel in scope.row.channel" :key="channel" size="mini"
:type="getChannelTag(channel)" class="channel-tag">
{{ getChannelName(channel) }}
</el-tag>
</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column width="80" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="editTemplate(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 快捷操作 -->
<el-col :span="24">
<el-card shadow="hover">
<div slot="header" class="clearfix">
<span>快捷操作</span>
</div>
<div class="quick-actions">
<el-button type="primary" icon="el-icon-message" @click="$router.push('/notification/send')">发送通知</el-button>
<el-button type="success" icon="el-icon-document-add" @click="$router.push('/notification/template')">创建模板</el-button>
<el-button type="warning" icon="el-icon-data-analysis" @click="exportNotificationData">导出数据</el-button>
<el-button type="info" icon="el-icon-setting" @click="openSettings">通知设置</el-button>
</div>
</el-card>
</el-col>
</el-row>
<!-- 设置对话框 -->
<el-dialog title="通知设置" :visible.sync="settingsVisible" width="500px">
<el-form :model="settings" label-width="120px">
<el-form-item label="默认通知渠道">
<el-checkbox-group v-model="settings.defaultChannels">
<el-checkbox :label="1">站内信</el-checkbox>
<el-checkbox :label="2">短信</el-checkbox>
<el-checkbox :label="3">邮件</el-checkbox>
<el-checkbox :label="4">推送</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="通知保留时间">
<el-input-number v-model="settings.retentionDays" :min="1" :max="365" label="天数"></el-input-number>
<span class="form-help">天</span>
</el-form-item>
<el-form-item label="自动清理通知">
<el-switch v-model="settings.autoCleanup"></el-switch>
</el-form-item>
<el-form-item label="通知发送限制">
<el-input-number v-model="settings.dailyLimit" :min="0" :max="10000" label="每日限制"></el-input-number>
<span class="form-help">条/天</span>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="settingsVisible = false">取消</el-button>
<el-button type="primary" @click="saveSettings">保存</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { getNotificationStats } from '@/api/notification'
import * as echarts from 'echarts'
export default {
name: 'NotificationDashboard',
data() {
return {
// 统计卡片数据
statisticsCards: [
{
title: '今日发送',
value: 128,
icon: 'el-icon-s-promotion',
color: '#409EFF',
change: 12.5
},
{
title: '阅读率',
value: '85.2%',
icon: 'el-icon-view',
color: '#67C23A',
change: 5.3
},
{
title: '模板数量',
value: 24,
icon: 'el-icon-document',
color: '#E6A23C',
change: 0
},
{
title: '未读通知',
value: 36,
icon: 'el-icon-message',
color: '#F56C6C',
change: -8.2
}
],
// 趋势图时间范围
trendTimeRange: 'week',
// 渠道分布图时间范围
channelTimeRange: 'week',
// 趋势图实例
trendChart: null,
// 渠道分布图实例
channelChart: null,
// 最近发送的通知
recentNotifications: [
{
id: 1,
title: '系统维护通知',
content: '系统将于2023年5月1日凌晨2点至4点进行维护,届时系统将暂停服务,请提前做好准备。',
type: 1,
channel: 1,
sendTime: '2023-04-29 10:30:00'
},
{
id: 2,
title: '快递到达通知',
content: '您的快递(顺丰 SF1234567890)已到达校园快递站,取件码:8888,请及时前往领取。',
type: 2,
channel: 2,
sendTime: '2023-04-29 09:15:00'
},
{
id: 3,
title: '五一活动邀请',
content: '诚邀您参加五一劳动节文艺汇演活动,时间:2023年5月1日下午2点,地点:校园大礼堂,期待您的参与!',
type: 3,
channel: 3,
sendTime: '2023-04-28 16:45:00'
},
{
id: 4,
title: '教务系统更新通知',
content: '教务系统已完成版本更新,新增成绩查询、课表导出等功能,欢迎使用。',
type: 1,
channel: 4,
sendTime: '2023-04-28 14:20:00'
}
],
// 最近更新的模板
recentTemplates: [
{
id: 1,
name: '系统维护通知模板',
content: '尊敬的{{userName}},系统将于{{startTime}}至{{endTime}}进行系统维护,届时系统将暂停服务,请提前做好准备。',
type: 1,
channel: [1, 3],
updateTime: '2023-04-27 16:30:00'
},
{
id: 2,
name: '快递到达通知模板',
content: '您好,{{userName}},您的快递({{expressCompany}} {{expressCode}})已到达校园快递站,取件码:{{pickupCode}},请及时前往领取。',
type: 2,
channel: [1, 2, 4],
updateTime: '2023-04-26 14:20:00'
},
{
id: 3,
name: '活动邀请模板',
content: '亲爱的{{userName}},诚邀您参加{{activityName}}活动,时间:{{activityTime}},地点:{{activityLocation}},期待您的参与!',
type: 3,
channel: [1, 3],
updateTime: '2023-04-25 11:15:00'
},
{
id: 4,
name: '课程变更通知模板',
content: '尊敬的{{userName}},您的课程{{courseName}}将于{{changeTime}}变更为{{newTime}},地点:{{location}},请知悉。',
type: 1,
channel: [1, 2, 3, 4],
updateTime: '2023-04-24 09:30:00'
}
],
// 设置对话框
settingsVisible: false,
// 设置
settings: {
defaultChannels: [1],
retentionDays: 30,
autoCleanup: true,
dailyLimit: 1000
}
}
},
mounted() {
this.initCharts()
window.addEventListener('resize', this.resizeCharts)
this.fetchStatistics()
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeCharts)
if (this.trendChart) {
this.trendChart.dispose()
}
if (this.channelChart) {
this.channelChart.dispose()
}
},
watch: {
trendTimeRange() {
this.updateTrendChart()
},
channelTimeRange() {
this.updateChannelChart()
}
},
methods: {
// 初始化图表
initCharts() {
// 初始化趋势图
this.trendChart = echarts.init(this.$refs.trendChart)
this.updateTrendChart()
// 初始化渠道分布图
this.channelChart = echarts.init(this.$refs.channelChart)
this.updateChannelChart()
},
// 更新趋势图
updateTrendChart() {
let xAxisData = []
let seriesData = []
if (this.trendTimeRange === 'week') {
xAxisData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
seriesData = [120, 132, 101, 134, 90, 30, 20]
} else if (this.trendTimeRange === 'month') {
xAxisData = Array.from({length: 30}, (_, i) => (i + 1) + '日')
seriesData = Array.from({length: 30}, () => Math.floor(Math.random() * 150) + 50)
} else {
xAxisData = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
seriesData = [320, 302, 301, 334, 390, 330, 320, 301, 302, 331, 340, 310]
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxisData,
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: '发送数量',
type: 'bar',
barWidth: '60%',
data: seriesData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
}
}
]
}
this.trendChart.setOption(option)
},
// 更新渠道分布图
updateChannelChart() {
let data = []
if (this.channelTimeRange === 'week') {
data = [
{ value: 40, name: '站内信' },
{ value: 30, name: '短信' },
{ value: 20, name: '邮件' },
{ value: 10, name: '推送' }
]
} else if (this.channelTimeRange === 'month') {
data = [
{ value: 35, name: '站内信' },
{ value: 25, name: '短信' },
{ value: 30, name: '邮件' },
{ value: 10, name: '推送' }
]
} else {
data = [
{ value: 30, name: '站内信' },
{ value: 25, name: '短信' },
{ value: 25, name: '邮件' },
{ value: 20, name: '推送' }
]
}
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
data: ['站内信', '短信', '邮件', '推送']
},
series: [
{
name: '通知渠道',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data,
itemStyle: {
normal: {
color: function(params) {
const colorList = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C']
return colorList[params.dataIndex]
}
}
}
}
]
}
this.channelChart.setOption(option)
},
// 调整图表大小
resizeCharts() {
if (this.trendChart) {
this.trendChart.resize()
}
if (this.channelChart) {
this.channelChart.resize()
}
},
// 获取统计数据
fetchStatistics() {
// 实际项目中的API调用
// getNotificationStats().then(response => {
// const data = response.data
// this.statisticsCards[0].value = data.todaySent
// this.statisticsCards[1].value = data.readRate + '%'
// this.statisticsCards[2].value = data.templateCount
// this.statisticsCards[3].value = data.unreadCount
// })
},
// 查看通知详情
viewDetail(row) {
this.$router.push({ path: `/notification/detail/${row.id}` })
},
// 编辑模板
editTemplate(row) {
this.$router.push({ path: '/notification/template', query: { id: row.id } })
},
// 查看更多
viewMore(type) {
if (type === 'list') {
this.$router.push({ path: '/notification/list' })
} else if (type === 'template') {
this.$router.push({ path: '/notification/template' })
}
},
// 导出通知数据
exportNotificationData() {
this.$message({
message: '通知数据导出成功',
type: 'success'
})
},
// 打开设置对话框
openSettings() {
this.settingsVisible = true
},
// 保存设置
saveSettings() {
this.$message({
message: '设置保存成功',
type: 'success'
})
this.settingsVisible = false
},
// 获取通知类型颜色
getTypeColor(type) {
switch (type) {
case 1: return '#409EFF'
case 2: return '#67C23A'
case 3: return '#E6A23C'
default: return '#909399'
}
},
// 获取通知类型标签类型
getTypeTag(type) {
switch (type) {
case 1: return 'primary'
case 2: return 'success'
case 3: return 'warning'
default: return 'info'
}
},
// 获取通知类型名称
getTypeName(type) {
switch (type) {
case 1: return '系统通知'
case 2: return '快递通知'
case 3: return '活动通知'
default: return '其他通知'
}
},
// 获取通知渠道标签类型
getChannelTag(channel) {
switch (channel) {
case 1: return 'primary'
case 2: return 'success'
case 3: return 'warning'
case 4: return 'danger'
default: return 'info'
}
},
// 获取通知渠道名称
getChannelName(channel) {
switch (channel) {
case 1: return '站内信'
case 2: return '短信'
case 3: return '邮件'
case 4: return '推送'
default: return '未知'
}
}
}
}
</script>
<style lang="scss" scoped>
.stat-card {
height: 120px;
margin-bottom: 20px;
.card-icon {
float: left;
font-size: 48px;
padding: 10px;
}
.card-content {
margin-left: 70px;
.card-title {
font-size: 14px;
color: #909399;
}
.card-value {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.card-footer {
font-size: 12px;
color: #909399;
i {
margin-left: 5px;
}
}
}
}
.chart-container {
padding: 10px;
}
.notification-item, .template-item {
padding: 5px 0;
.notification-title, .template-title {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
span {
font-weight: bold;
}
}
.notification-content, .template-content {
font-size: 12px;
color: #606266;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-footer, .template-footer {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #909399;
}
}
.quick-actions {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
.el-button {
margin: 10px;
}
}
.channel-tag {
margin-right: 5px;
}
.form-help {
margin-left: 10px;
color: #909399;
font-size: 14px;
}
</style>
express-ui\src\views\notification\detail.vue
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>通知详情</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="goBack">返回</el-button>
</div>
<el-row :gutter="20" v-loading="loading">
<el-col :span="24">
<el-descriptions :column="2" border>
<el-descriptions-item label="通知ID" :span="1">{{ notification.id }}</el-descriptions-item>
<el-descriptions-item label="通知类型" :span="1">
<el-tag :type="notification.type === 1 ? 'primary' : notification.type === 2 ? 'success' : 'info'">
{{ typeFormat(notification) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="通知标题" :span="2">{{ notification.title }}</el-descriptions-item>
<el-descriptions-item label="通知渠道" :span="1">
<el-tag v-if="notification.channel === 1" type="primary">站内信</el-tag>
<el-tag v-else-if="notification.channel === 2" type="success">短信</el-tag>
<el-tag v-else-if="notification.channel === 3" type="warning">邮件</el-tag>
<el-tag v-else-if="notification.channel === 4" type="danger">推送</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发送状态" :span="1">
<el-tag :type="notification.status === 0 ? 'info' : notification.status === 1 ? 'success' : 'danger'">
{{ statusFormat(notification) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="接收人" :span="1">{{ notification.receiverName }}</el-descriptions-item>
<el-descriptions-item label="发送时间" :span="1">{{ notification.sendTime }}</el-descriptions-item>
<el-descriptions-item label="通知内容" :span="2">
<div class="notification-content">{{ notification.content }}</div>
</el-descriptions-item>
<el-descriptions-item label="使用模板" :span="2" v-if="notification.templateId">
<el-link type="primary" @click="viewTemplate">{{ notification.templateName }}</el-link>
</el-descriptions-item>
</el-descriptions>
</el-col>
</el-row>
<el-divider content-position="center">发送记录</el-divider>
<el-table :data="sendRecords" style="width: 100%" border>
<el-table-column prop="id" label="记录ID" width="80" align="center" />
<el-table-column prop="channelName" label="发送渠道" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="getChannelTagType(scope.row.channel)">{{ scope.row.channelName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="sendTime" label="发送时间" width="180" align="center" />
<el-table-column prop="status" label="发送状态" width="120" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '发送成功' : '发送失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" />
</el-table>
<el-divider content-position="center">阅读记录</el-divider>
<el-table :data="readRecords" style="width: 100%" border>
<el-table-column prop="id" label="记录ID" width="80" align="center" />
<el-table-column prop="userName" label="用户" width="120" align="center" />
<el-table-column prop="readTime" label="阅读时间" width="180" align="center" />
<el-table-column prop="readDevice" label="阅读设备" width="180" align="center" />
<el-table-column prop="remark" label="备注" />
</el-table>
<div class="action-buttons">
<el-button type="primary" icon="el-icon-edit" @click="handleEdit">编辑通知</el-button>
<el-button type="success" icon="el-icon-refresh" @click="handleResend">重新发送</el-button>
<el-button type="danger" icon="el-icon-delete" @click="handleDelete">删除通知</el-button>
</div>
<!-- 模板详情对话框 -->
<el-dialog title="模板详情" :visible.sync="templateDialogVisible" width="600px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="模板名称">{{ template.name }}</el-descriptions-item>
<el-descriptions-item label="模板类型">{{ typeFormat(template) }}</el-descriptions-item>
<el-descriptions-item label="适用渠道" :span="2">
<el-tag v-if="template.channel && template.channel.includes(1)" type="primary" class="channel-tag">站内信</el-tag>
<el-tag v-if="template.channel && template.channel.includes(2)" type="success" class="channel-tag">短信</el-tag>
<el-tag v-if="template.channel && template.channel.includes(3)" type="warning" class="channel-tag">邮件</el-tag>
<el-tag v-if="template.channel && template.channel.includes(4)" type="danger" class="channel-tag">推送</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ template.createTime }}</el-descriptions-item>
<el-descriptions-item label="模板内容" :span="2">
<div style="white-space: pre-wrap;">{{ template.content }}</div>
</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button @click="templateDialogVisible = false">关闭</el-button>
</div>
</el-dialog>
</el-card>
</div>
</template>
<script>
import { getNotification, deleteNotification, resendNotification } from '@/api/notification'
import { getTemplate } from '@/api/notification'
export default {
name: 'NotificationDetail',
data() {
return {
loading: true,
notification: {
id: undefined,
title: '',
content: '',
type: undefined,
channel: undefined,
receiverId: undefined,
receiverName: '',
sendTime: '',
status: undefined,
templateId: undefined,
templateName: ''
},
// 发送记录
sendRecords: [],
// 阅读记录
readRecords: [],
// 模板详情对话框
templateDialogVisible: false,
// 模板信息
template: {
id: undefined,
name: '',
type: undefined,
channel: [],
content: '',
createTime: ''
},
// 通知类型选项
typeOptions: [
{ value: '1', label: '系统通知' },
{ value: '2', label: '快递通知' },
{ value: '3', label: '活动通知' }
],
// 通知状态选项
statusOptions: [
{ value: '0', label: '草稿' },
{ value: '1', label: '已发送' },
{ value: '2', label: '发送失败' }
]
}
},
created() {
this.getNotificationDetail()
},
methods: {
// 获取通知详情
getNotificationDetail() {
const id = this.$route.params.id
if (id) {
this.loading = true
// 模拟数据,实际项目中应该调用API
setTimeout(() => {
this.notification = {
id: id,
title: '快递到达通知',
content: '您好,张三,您的快递(顺丰 SF1234567890)已到达校园快递站,取件码:8888,请及时前往领取。',
type: 2,
channel: 2,
receiverId: 2,
receiverName: '张三',
sendTime: '2023-04-29 14:30:00',
status: 1,
templateId: 2,
templateName: '快递到达通知模板'
}
// 模拟发送记录
this.sendRecords = [
{
id: 1,
channel: 2,
channelName: '短信',
sendTime: '2023-04-29 14:30:00',
status: 1,
remark: '发送成功'
},
{
id: 2,
channel: 1,
channelName: '站内信',
sendTime: '2023-04-29 14:30:05',
status: 1,
remark: '发送成功'
},
{
id: 3,
channel: 4,
channelName: '推送',
sendTime: '2023-04-29 14:30:10',
status: 1,
remark: '发送成功'
}
]
// 模拟阅读记录
this.readRecords = [
{
id: 1,
userName: '张三',
readTime: '2023-04-29 15:20:30',
readDevice: 'iPhone 13',
remark: 'APP内阅读'
}
]
this.loading = false
}, 500)
// 实际项目中的API调用
// getNotification(id).then(response => {
// this.notification = response.data
// this.loading = false
// })
}
},
// 查看模板详情
viewTemplate() {
if (this.notification.templateId) {
// 模拟数据,实际项目中应该调用API
this.template = {
id: this.notification.templateId,
name: '快递到达通知模板',
type: 2,
channel: [1, 2, 4],
content: '您好,{{userName}},您的快递({{expressCompany}} {{expressCode}})已到达校园快递站,取件码:{{pickupCode}},请及时前往领取。',
createTime: '2023-04-21 14:30:00'
}
this.templateDialogVisible = true
// 实际项目中的API调用
// getTemplate(this.notification.templateId).then(response => {
// this.template = response.data
// this.templateDialogVisible = true
// })
}
},
// 编辑通知
handleEdit() {
this.$router.push({ path: '/notification/list', query: { id: this.notification.id, edit: true } })
},
// 重新发送通知
handleResend() {
this.$confirm('确认重新发送该通知?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中的API调用
// resendNotification(this.notification.id).then(response => {
// this.$message.success('重新发送成功')
// this.getNotificationDetail()
// })
// 模拟发送成功
this.$message.success('重新发送成功')
// 添加一条新的发送记录
const newRecord = {
id: this.sendRecords.length + 1,
channel: this.notification.channel,
channelName: this.getChannelName(this.notification.channel),
sendTime: new Date().toLocaleString(),
status: 1,
remark: '重新发送'
}
this.sendRecords.unshift(newRecord)
}).catch(() => {})
},
// 删除通知
handleDelete() {
this.$confirm('确认删除该通知?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中的API调用
// deleteNotification(this.notification.id).then(response => {
// this.$message.success('删除成功')
// this.goBack()
// })
// 模拟删除成功
this.$message.success('删除成功')
this.goBack()
}).catch(() => {})
},
// 返回列表页
goBack() {
this.$router.push({ path: '/notification/list' })
},
// 通知类型字典翻译
typeFormat(row) {
return this.selectDictLabel(this.typeOptions, row.type)
},
// 通知状态字典翻译
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, row.status)
},
// 字典翻译
selectDictLabel(datas, value) {
const actions = []
Object.keys(datas).some(key => {
if (datas[key].value == value) {
actions.push(datas[key].label)
return true
}
})
return actions.join('')
},
// 获取渠道标签类型
getChannelTagType(channel) {
switch (channel) {
case 1: return 'primary'
case 2: return 'success'
case 3: return 'warning'
case 4: return 'danger'
default: return 'info'
}
},
// 获取渠道名称
getChannelName(channel) {
switch (channel) {
case 1: return '站内信'
case 2: return '短信'
case 3: return '邮件'
case 4: return '推送'
default: return '未知'
}
}
}
}
</script>
<style lang="scss" scoped>
.notification-content {
white-space: pre-wrap;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
min-height: 100px;
}
.action-buttons {
margin-top: 20px;
text-align: center;
}
.channel-tag {
margin-right: 5px;
}
.el-divider {
margin: 24px 0;
}
</style>
express-ui\src\views\notification\history.vue
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>通知历史记录</span>
<div class="header-operations">
<el-input
placeholder="搜索通知内容"
v-model="searchQuery"
clearable
style="width: 200px;"
@clear="handleClear"
@keyup.enter.native="handleSearch">
<el-button slot="append" icon="el-icon-search" @click="handleSearch"></el-button>
</el-input>
<el-button type="danger" icon="el-icon-delete" @click="handleBatchDelete" :disabled="selectedNotifications.length === 0">批量删除</el-button>
<el-button type="primary" icon="el-icon-refresh" @click="refreshList">刷新</el-button>
</div>
</div>
<!-- 筛选条件 -->
<el-form :inline="true" class="filter-container">
<el-form-item label="通知类型">
<el-select v-model="filters.type" placeholder="全部类型" clearable @change="handleFilterChange">
<el-option label="系统通知" :value="1"></el-option>
<el-option label="快递通知" :value="2"></el-option>
<el-option label="活动通知" :value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item label="通知渠道">
<el-select v-model="filters.channel" placeholder="全部渠道" clearable @change="handleFilterChange">
<el-option label="站内信" :value="1"></el-option>
<el-option label="短信" :value="2"></el-option>
<el-option label="邮件" :value="3"></el-option>
<el-option label="推送" :value="4"></el-option>
</el-select>
</el-form-item>
<el-form-item label="阅读状态">
<el-select v-model="filters.read" placeholder="全部状态" clearable @change="handleFilterChange">
<el-option label="已读" :value="true"></el-option>
<el-option label="未读" :value="false"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="filters.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="handleFilterChange">
</el-date-picker>
</el-form-item>
</el-form>
<!-- 通知列表 -->
<el-table
v-loading="loading"
:data="notificationList"
style="width: 100%"
@selection-change="handleSelectionChange"
:row-class-name="getRowClassName">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="状态" width="60" align="center">
<template slot-scope="scope">
<el-badge is-dot :hidden="scope.row.read" class="status-badge">
<i class="el-icon-message" :class="{ 'read': scope.row.read }"></i>
</el-badge>
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template slot-scope="scope">
<el-tag :type="getTypeTagType(scope.row.type)" size="small">
{{ getTypeName(scope.row.type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="渠道" width="100">
<template slot-scope="scope">
<el-tag :type="getChannelTagType(scope.row.channel)" size="small">
{{ getChannelName(scope.row.channel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" show-overflow-tooltip></el-table-column>
<el-table-column prop="sendTime" label="发送时间" width="170" sortable>
<template slot-scope="scope">
{{ formatDate(scope.row.sendTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button
v-if="!scope.row.read"
size="mini"
type="success"
@click="markAsRead(scope.row)"
icon="el-icon-check">
标为已读
</el-button>
<el-button
size="mini"
type="primary"
@click="viewDetail(scope.row)"
icon="el-icon-view">
查看
</el-button>
<el-button
size="mini"
type="danger"
@click="deleteNotification(scope.row)"
icon="el-icon-delete">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 30, 50]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
</el-card>
<!-- 通知详情对话框 -->
<el-dialog title="通知详情" :visible.sync="detailDialogVisible" width="600px" append-to-body>
<div class="notification-detail">
<div class="detail-header">
<h3 class="detail-title">{{ currentNotification.title }}</h3>
<div class="detail-meta">
<span>
<i class="el-icon-time"></i>
{{ formatDate(currentNotification.sendTime) }}
</span>
<span>
<i class="el-icon-user"></i>
{{ currentNotification.sender }}
</span>
<span>
<el-tag :type="getTypeTagType(currentNotification.type)" size="small">
{{ getTypeName(currentNotification.type) }}
</el-tag>
</span>
<span>
<el-tag :type="getChannelTagType(currentNotification.channel)" size="small">
{{ getChannelName(currentNotification.channel) }}
</el-tag>
</span>
</div>
</div>
<div class="detail-content" v-html="formatContent(currentNotification.content)"></div>
<div v-if="currentNotification.attachments && currentNotification.attachments.length > 0" class="detail-attachments">
<h4>附件</h4>
<ul class="attachment-list">
<li v-for="(attachment, index) in currentNotification.attachments" :key="index" class="attachment-item">
<i class="el-icon-document"></i>
<span class="attachment-name">{{ attachment.name }}</span>
<span class="attachment-size">{{ formatFileSize(attachment.size) }}</span>
<el-button type="text" @click="downloadAttachment(attachment)">下载</el-button>
</li>
</ul>
</div>
<div class="detail-actions">
<el-button v-if="!currentNotification.read" type="success" @click="markAsReadInDialog">标为已读</el-button>
<el-button type="primary" @click="replyNotification">回复</el-button>
<el-button type="danger" @click="deleteNotificationInDialog">删除</el-button>
</div>
</div>
</el-dialog>
<!-- 回复对话框 -->
<el-dialog title="回复通知" :visible.sync="replyDialogVisible" width="500px" append-to-body>
<el-form :model="replyForm" :rules="replyRules" ref="replyForm" label-width="80px">
<el-form-item label="标题" prop="title">
<el-input v-model="replyForm.title" placeholder="请输入回复标题"></el-input>
</el-form-item>
<el-form-item label="内容" prop="content">
<el-input type="textarea" v-model="replyForm.content" :rows="5" placeholder="请输入回复内容"></el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="replyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitReply">发送回复</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { NotificationType, NotificationChannel, getTypeTagType, getChannelTagType, formatContent, formatFileSize } from '@/utils/notification'
export default {
name: 'NotificationHistory',
data() {
return {
// 加载状态
loading: false,
// 搜索关键词
searchQuery: '',
// 筛选条件
filters: {
type: null,
channel: null,
read: null,
dateRange: null
},
// 分页信息
pagination: {
currentPage: 1,
pageSize: 10,
total: 0
},
// 通知列表
notificationList: [],
// 选中的通知
selectedNotifications: [],
// 当前查看的通知
currentNotification: {},
// 对话框显示状态
detailDialogVisible: false,
replyDialogVisible: false,
// 回复表单
replyForm: {
title: '',
content: ''
},
// 回复表单校验规则
replyRules: {
title: [
{ required: true, message: '请输入回复标题', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入回复内容', trigger: 'blur' }
]
}
}
},
created() {
this.fetchNotifications()
},
methods: {
// 获取通知列表
fetchNotifications() {
this.loading = true
// 构建查询参数
const params = {
page: this.pagination.currentPage,
size: this.pagination.pageSize,
query: this.searchQuery
}
// 添加筛选条件
if (this.filters.type !== null) {
params.type = this.filters.type
}
if (this.filters.channel !== null) {
params.channel = this.filters.channel
}
if (this.filters.read !== null) {
params.read = this.filters.read
}
if (this.filters.dateRange) {
params.startDate = this.filters.dateRange[0]
params.endDate = this.filters.dateRange[1]
}
// 实际项目中应该调用API获取通知列表
// getNotificationHistory(params).then(response => {
// this.notificationList = response.data.records
// this.pagination.total = response.data.total
// this.loading = false
// })
// 模拟获取数据
setTimeout(() => {
// 生成模拟数据
this.notificationList = this.generateMockData()
this.pagination.total = 85
this.loading = false
}, 500)
},
// 生成模拟数据
generateMockData() {
const result = []
const types = [1, 2, 3]
const channels = [1, 2, 3, 4]
const titles = [
'系统维护通知',
'您的快递已到达',
'校园活动邀请',
'账号安全提醒',
'新功能上线通知',
'快递即将过期提醒',
'讲座活动预告',
'系统升级公告'
]
const senders = ['系统管理员', '快递服务', '学生会', '安全中心', '技术部门']
for (let i = 0; i < this.pagination.pageSize; i++) {
const type = types[Math.floor(Math.random() * types.length)]
const channel = channels[Math.floor(Math.random() * channels.length)]
const title = titles[Math.floor(Math.random() * titles.length)]
const sender = senders[Math.floor(Math.random() * senders.length)]
const read = Math.random() > 0.3
// 生成随机日期(最近30天内)
const date = new Date()
date.setDate(date.getDate() - Math.floor(Math.random() * 30))
// 生成随机内容
let content = ''
if (type === 1) {
content = '尊敬的用户,系统将于2025年4月15日凌晨2:00-4:00进行例行维护,届时系统将暂停服务。给您带来的不便,敬请谅解。'
} else if (type === 2) {
content = '您好,您的快递(顺丰速运 - SF1234567890)已到达校园快递中心,请凭取件码 8888 及时领取。取件时间:9:00-18:00。'
} else {
content = '诚邀您参加"校园科技创新大赛",时间:2025年4月20日14:00,地点:图书馆报告厅。欢迎踊跃参与!'
}
// 随机添加附件
const attachments = []
if (Math.random() > 0.7) {
attachments.push({
name: '附件1.pdf',
size: Math.floor(Math.random() * 1000000) + 100000,
url: 'https://example.com/attachment1.pdf'
})
}
result.push({
id: i + 1,
type,
channel,
title,
content,
sender,
read,
sendTime: date.toISOString(),
readTime: read ? new Date(date.getTime() + Math.floor(Math.random() * 86400000)).toISOString() : null,
attachments
})
}
// 应用筛选
return this.applyFilters(result)
},
// 应用筛选条件
applyFilters(data) {
let result = [...data]
// 搜索关键词
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase()
result = result.filter(item =>
item.title.toLowerCase().includes(query) ||
item.content.toLowerCase().includes(query)
)
}
// 通知类型
if (this.filters.type !== null) {
result = result.filter(item => item.type === this.filters.type)
}
// 通知渠道
if (this.filters.channel !== null) {
result = result.filter(item => item.channel === this.filters.channel)
}
// 阅读状态
if (this.filters.read !== null) {
result = result.filter(item => item.read === this.filters.read)
}
// 日期范围
if (this.filters.dateRange) {
const startDate = new Date(this.filters.dateRange[0])
const endDate = new Date(this.filters.dateRange[1])
endDate.setHours(23, 59, 59, 999) // 设置为当天结束时间
result = result.filter(item => {
const sendTime = new Date(item.sendTime)
return sendTime >= startDate && sendTime <= endDate
})
}
return result
},
// 处理搜索
handleSearch() {
this.pagination.currentPage = 1
this.fetchNotifications()
},
// 处理清除搜索
handleClear() {
this.searchQuery = ''
this.handleSearch()
},
// 处理筛选条件变化
handleFilterChange() {
this.pagination.currentPage = 1
this.fetchNotifications()
},
// 处理页码变化
handleCurrentChange(page) {
this.pagination.currentPage = page
this.fetchNotifications()
},
// 处理每页显示数量变化
handleSizeChange(size) {
this.pagination.pageSize = size
this.pagination.currentPage = 1
this.fetchNotifications()
},
// 处理选择变化
handleSelectionChange(selection) {
this.selectedNotifications = selection
},
// 获取行类名
getRowClassName({ row }) {
return row.read ? '' : 'unread-row'
},
// 刷新列表
refreshList() {
this.fetchNotifications()
},
// 查看通知详情
viewDetail(notification) {
this.currentNotification = { ...notification }
this.detailDialogVisible = true
// 如果是未读通知,自动标记为已读
if (!notification.read) {
this.markAsRead(notification, false)
}
},
// 标记为已读
markAsRead(notification, showMessage = true) {
// 实际项目中应该调用API标记为已读
// markNotificationAsRead(notification.id).then(response => {
// notification.read = true
// if (showMessage) {
// this.$message.success('已标记为已读')
// }
// })
// 模拟标记为已读
notification.read = true
notification.readTime = new Date().toISOString()
if (showMessage) {
this.$message({
message: '已标记为已读',
type: 'success'
})
}
},
// 在对话框中标记为已读
markAsReadInDialog() {
this.markAsRead(this.currentNotification)
this.currentNotification.read = true
this.currentNotification.readTime = new Date().toISOString()
},
// 删除通知
deleteNotification(notification) {
this.$confirm('确定要删除此通知吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中应该调用API删除通知
// deleteNotification(notification.id).then(response => {
// this.$message.success('删除成功')
// this.fetchNotifications()
// })
// 模拟删除
const index = this.notificationList.findIndex(item => item.id === notification.id)
if (index !== -1) {
this.notificationList.splice(index, 1)
}
this.$message({
message: '删除成功',
type: 'success'
})
}).catch(() => {})
},
// 在对话框中删除通知
deleteNotificationInDialog() {
this.deleteNotification(this.currentNotification)
this.detailDialogVisible = false
},
// 批量删除通知
handleBatchDelete() {
if (this.selectedNotifications.length === 0) {
return
}
this.$confirm(`确定要删除选中的 ${this.selectedNotifications.length} 条通知吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中应该调用API批量删除通知
// const ids = this.selectedNotifications.map(item => item.id)
// batchDeleteNotifications(ids).then(response => {
// this.$message.success('批量删除成功')
// this.fetchNotifications()
// })
// 模拟批量删除
const ids = this.selectedNotifications.map(item => item.id)
this.notificationList = this.notificationList.filter(item => !ids.includes(item.id))
this.selectedNotifications = []
this.$message({
message: '批量删除成功',
type: 'success'
})
}).catch(() => {})
},
// 回复通知
replyNotification() {
this.replyForm = {
title: `回复:${this.currentNotification.title}`,
content: ''
}
this.replyDialogVisible = true
},
// 提交回复
submitReply() {
this.$refs.replyForm.validate(valid => {
if (valid) {
// 实际项目中应该调用API发送回复
// const data = {
// originalId: this.currentNotification.id,
// title: this.replyForm.title,
// content: this.replyForm.content
// }
// replyNotification(data).then(response => {
// this.$message.success('回复已发送')
// this.replyDialogVisible = false
// })
// 模拟发送回复
this.$message({
message: '回复已发送',
type: 'success'
})
this.replyDialogVisible = false
}
})
},
// 下载附件
downloadAttachment(attachment) {
// 实际项目中应该调用API下载附件
// window.open(attachment.url, '_blank')
// 模拟下载
this.$message({
message: '开始下载:' + attachment.name,
type: 'success'
})
},
// 格式化日期
formatDate(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
// 获取通知类型名称
getTypeName(type) {
switch (type) {
case 1: return '系统通知'
case 2: return '快递通知'
case 3: return '活动通知'
default: return '其他通知'
}
},
// 获取通知渠道名称
getChannelName(channel) {
switch (channel) {
case 1: return '站内信'
case 2: return '短信'
case 3: return '邮件'
case 4: return '推送'
default: return '其他'
}
},
// 获取通知类型对应的标签类型
getTypeTagType,
// 获取通知渠道对应的标签类型
getChannelTagType,
// 格式化内容
formatContent,
// 格式化文件大小
formatFileSize
}
}
</script>
<style lang="scss" scoped>
.header-operations {
float: right;
display: flex;
align-items: center;
.el-button {
margin-left: 10px;
}
}
.filter-container {
margin-bottom: 20px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
}
.pagination-container {
margin-top: 20px;
text-align: center;
}
.status-badge {
i {
font-size: 18px;
color: #409EFF;
&.read {
color: #909399;
}
}
}
.notification-detail {
.detail-header {
margin-bottom: 20px;
.detail-title {
margin: 0 0 10px 0;
font-size: 18px;
}
.detail-meta {
display: flex;
flex-wrap: wrap;
color: #909399;
font-size: 14px;
span {
margin-right: 15px;
margin-bottom: 5px;
display: flex;
align-items: center;
i {
margin-right: 5px;
}
}
}
}
.detail-content {
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
min-height: 100px;
line-height: 1.6;
white-space: pre-wrap;
margin-bottom: 20px;
}
.detail-attachments {
margin-bottom: 20px;
h4 {
margin: 0 0 10px 0;
font-size: 16px;
}
.attachment-list {
list-style: none;
padding: 0;
margin: 0;
.attachment-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
i {
margin-right: 10px;
color: #909399;
}
.attachment-name {
flex: 1;
}
.attachment-size {
color: #909399;
margin: 0 10px;
}
}
}
}
.detail-actions {
text-align: right;
}
}
</style>
<style>
.unread-row {
font-weight: bold;
background-color: #f0f9eb;
}
</style>
express-ui\src\views\notification\list.vue
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>通知列表</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAdd">新增通知</el-button>
</div>
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="标题" prop="title">
<el-input v-model="queryParams.title" placeholder="请输入通知标题" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="queryParams.type" placeholder="通知类型" clearable size="small">
<el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="通知状态" clearable size="small">
<el-option v-for="dict in statusOptions" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<el-form-item label="发送时间">
<el-date-picker
v-model="dateRange"
size="small"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格工具栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="notificationList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" align="center" prop="id" width="80" />
<el-table-column label="标题" align="center" prop="title" :show-overflow-tooltip="true" />
<el-table-column label="通知类型" align="center" prop="type">
<template slot-scope="scope">
<el-tag :type="scope.row.type === 1 ? 'primary' : scope.row.type === 2 ? 'success' : 'info'">
{{ typeFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="通知渠道" align="center" prop="channel">
<template slot-scope="scope">
<el-tag v-if="scope.row.channel === 1" type="primary">站内信</el-tag>
<el-tag v-else-if="scope.row.channel === 2" type="success">短信</el-tag>
<el-tag v-else-if="scope.row.channel === 3" type="warning">邮件</el-tag>
<el-tag v-else-if="scope.row.channel === 4" type="danger">推送</el-tag>
</template>
</el-table-column>
<el-table-column label="接收人" align="center" prop="receiverName" :show-overflow-tooltip="true" />
<el-table-column label="发送时间" align="center" prop="sendTime" width="160">
<template slot-scope="scope">
<span>{{ scope.row.sendTime }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 0 ? 'info' : scope.row.status === 1 ? 'success' : 'danger'">
{{ statusFormat(scope.row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.row)">查看</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改通知对话框 -->
<el-dialog :title="title" :visible.sync="open" width="780px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="通知标题" prop="title">
<el-input v-model="form.title" placeholder="请输入通知标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="通知类型" prop="type">
<el-select v-model="form.type" placeholder="请选择通知类型">
<el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="通知渠道" prop="channel">
<el-select v-model="form.channel" placeholder="请选择通知渠道">
<el-option label="站内信" :value="1" />
<el-option label="短信" :value="2" />
<el-option label="邮件" :value="3" />
<el-option label="推送" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="接收人" prop="receiverId">
<el-select v-model="form.receiverId" placeholder="请选择接收人" filterable>
<el-option v-for="item in userOptions" :key="item.userId" :label="item.userName" :value="item.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="通知内容" prop="content">
<el-input v-model="form.content" type="textarea" placeholder="请输入通知内容" :rows="8" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="发送时间" prop="sendTime">
<el-date-picker v-model="form.sendTime" type="datetime" placeholder="选择发送时间" value-format="yyyy-MM-dd HH:mm:ss" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="0">草稿</el-radio>
<el-radio :label="1">已发送</el-radio>
<el-radio :label="2">发送失败</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<!-- 通知详情对话框 -->
<el-dialog title="通知详情" :visible.sync="openView" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="通知标题">{{ form.title }}</el-descriptions-item>
<el-descriptions-item label="通知类型">{{ typeFormat(form) }}</el-descriptions-item>
<el-descriptions-item label="通知渠道">
<el-tag v-if="form.channel === 1" type="primary">站内信</el-tag>
<el-tag v-else-if="form.channel === 2" type="success">短信</el-tag>
<el-tag v-else-if="form.channel === 3" type="warning">邮件</el-tag>
<el-tag v-else-if="form.channel === 4" type="danger">推送</el-tag>
</el-descriptions-item>
<el-descriptions-item label="接收人">{{ form.receiverName }}</el-descriptions-item>
<el-descriptions-item label="发送时间" :span="1">{{ form.sendTime }}</el-descriptions-item>
<el-descriptions-item label="状态" :span="1">
<el-tag :type="form.status === 0 ? 'info' : form.status === 1 ? 'success' : 'danger'">
{{ statusFormat(form) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="通知内容" :span="2">
<div style="white-space: pre-wrap;">{{ form.content }}</div>
</el-descriptions-item>
</el-descriptions>
<div slot="footer" class="dialog-footer">
<el-button @click="openView = false">关 闭</el-button>
</div>
</el-dialog>
</el-card>
</div>
</template>
<script>
import { listNotification, getNotification, delNotification, addNotification, updateNotification } from '@/api/notification'
export default {
name: 'NotificationList',
data() {
return {
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 通知表格数据
notificationList: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 是否显示详情弹出层
openView: false,
// 日期范围
dateRange: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
title: undefined,
type: undefined,
status: undefined
},
// 表单参数
form: {},
// 表单校验
rules: {
title: [
{ required: true, message: '通知标题不能为空', trigger: 'blur' }
],
content: [
{ required: true, message: '通知内容不能为空', trigger: 'blur' }
],
type: [
{ required: true, message: '通知类型不能为空', trigger: 'change' }
],
channel: [
{ required: true, message: '通知渠道不能为空', trigger: 'change' }
],
receiverId: [
{ required: true, message: '接收人不能为空', trigger: 'change' }
]
},
// 通知类型选项
typeOptions: [
{ value: '1', label: '系统通知' },
{ value: '2', label: '快递通知' },
{ value: '3', label: '活动通知' }
],
// 通知状态选项
statusOptions: [
{ value: '0', label: '草稿' },
{ value: '1', label: '已发送' },
{ value: '2', label: '发送失败' }
],
// 用户选项
userOptions: [
{ userId: 1, userName: '管理员' },
{ userId: 2, userName: '张三' },
{ userId: 3, userName: '李四' },
{ userId: 4, userName: '王五' }
]
}
},
created() {
this.getList()
},
methods: {
/** 查询通知列表 */
getList() {
this.loading = true
// 模拟数据,实际项目中应该调用API
this.notificationList = [
{
id: 1,
title: '系统维护通知',
type: 1,
channel: 1,
receiverId: 1,
receiverName: '管理员',
content: '尊敬的用户,系统将于2023年5月1日22:00-24:00进行系统维护,届时系统将暂停服务,请提前做好准备。',
sendTime: '2023-04-28 10:00:00',
status: 1
},
{
id: 2,
title: '快递到达通知',
type: 2,
channel: 2,
receiverId: 2,
receiverName: '张三',
content: '您的快递已到达校园快递站,请及时前往领取。',
sendTime: '2023-04-29 14:30:00',
status: 1
},
{
id: 3,
title: '活动邀请',
type: 3,
channel: 3,
receiverId: 3,
receiverName: '李四',
content: '诚邀您参加校园快递服务满意度调查活动,完成问卷可获得积分奖励。',
sendTime: '2023-04-30 09:15:00',
status: 0
}
]
this.total = this.notificationList.length
this.loading = false
// 实际项目中的API调用
// listNotification(this.queryParams).then(response => {
// this.notificationList = response.data.rows
// this.total = response.data.total
// this.loading = false
// })
},
// 通知类型字典翻译
typeFormat(row) {
return this.selectDictLabel(this.typeOptions, row.type)
},
// 通知状态字典翻译
statusFormat(row) {
return this.selectDictLabel(this.statusOptions, row.status)
},
// 字典翻译
selectDictLabel(datas, value) {
const actions = []
Object.keys(datas).some(key => {
if (datas[key].value == value) {
actions.push(datas[key].label)
return true
}
})
return actions.join('')
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm('queryForm')
this.handleQuery()
},
/** 新增按钮操作 */
handleAdd() {
this.reset()
this.open = true
this.title = '添加通知'
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset()
const id = row.id || this.ids[0]
// 实际项目中应该调用API获取详情
// getNotification(id).then(response => {
// this.form = response.data
// this.open = true
// this.title = '修改通知'
// })
// 模拟数据
this.form = JSON.parse(JSON.stringify(row))
this.open = true
this.title = '修改通知'
},
/** 查看详情按钮操作 */
handleView(row) {
this.reset()
const id = row.id
// 实际项目中应该调用API获取详情
// getNotification(id).then(response => {
// this.form = response.data
// this.openView = true
// })
// 模拟数据
this.form = JSON.parse(JSON.stringify(row))
this.openView = true
},
/** 提交按钮 */
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.id) {
// updateNotification(this.form).then(response => {
// this.$modal.msgSuccess('修改成功')
// this.open = false
// this.getList()
// })
this.$message.success('修改成功')
this.open = false
this.getList()
} else {
// addNotification(this.form).then(response => {
// this.$modal.msgSuccess('新增成功')
// this.open = false
// this.getList()
// })
this.$message.success('新增成功')
this.open = false
this.getList()
}
}
})
},
/** 删除按钮操作 */
handleDelete(row) {
const ids = row.id || this.ids
this.$confirm('是否确认删除通知编号为"' + ids + '"的数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// delNotification(ids).then(() => {
// this.getList()
// this.$modal.msgSuccess('删除成功')
// })
this.$message.success('删除成功')
this.getList()
}).catch(() => {})
},
/** 导出按钮操作 */
handleExport() {
this.$confirm('是否确认导出所有通知数据项?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message.success('导出成功')
}).catch(() => {})
},
// 多选框选中数据
handleSelectionChange(selection) {
this.ids = selection.map(item => item.id)
this.single = selection.length !== 1
this.multiple = !selection.length
},
/** 重置表单数据 */
reset() {
this.form = {
id: undefined,
title: undefined,
content: undefined,
type: 1,
channel: 1,
receiverId: undefined,
receiverName: undefined,
sendTime: undefined,
status: 0
}
this.resetForm('form')
},
/** 取消按钮 */
cancel() {
this.open = false
this.reset()
}
}
}
</script>
express-ui\src\views\notification\my-notifications.vue
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>我的通知</span>
<div class="header-operations">
<el-button type="text" @click="markAllAsRead" v-if="unreadCount > 0">全部标为已读</el-button>
<el-button type="text" @click="refreshNotifications">刷新</el-button>
</div>
</div>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="全部通知" name="all">
<div class="tab-content">
<el-empty v-if="notifications.length === 0" description="暂无通知"></el-empty>
<notification-list v-else :notifications="notifications" @view="viewNotification" @mark-read="markAsRead" />
</div>
</el-tab-pane>
<el-tab-pane :label="'未读通知 (' + unreadCount + ')'" name="unread">
<div class="tab-content">
<el-empty v-if="unreadNotifications.length === 0" description="暂无未读通知"></el-empty>
<notification-list v-else :notifications="unreadNotifications" @view="viewNotification" @mark-read="markAsRead" />
</div>
</el-tab-pane>
<el-tab-pane label="系统通知" name="system">
<div class="tab-content">
<el-empty v-if="systemNotifications.length === 0" description="暂无系统通知"></el-empty>
<notification-list v-else :notifications="systemNotifications" @view="viewNotification" @mark-read="markAsRead" />
</div>
</el-tab-pane>
<el-tab-pane label="快递通知" name="express">
<div class="tab-content">
<el-empty v-if="expressNotifications.length === 0" description="暂无快递通知"></el-empty>
<notification-list v-else :notifications="expressNotifications" @view="viewNotification" @mark-read="markAsRead" />
</div>
</el-tab-pane>
<el-tab-pane label="活动通知" name="activity">
<div class="tab-content">
<el-empty v-if="activityNotifications.length === 0" description="暂无活动通知"></el-empty>
<notification-list v-else :notifications="activityNotifications" @view="viewNotification" @mark-read="markAsRead" />
</div>
</el-tab-pane>
</el-tabs>
<div class="pagination-container">
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryParams.pageNum"
:page-sizes="[10, 20, 30, 50]"
:page-size="queryParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-card>
<!-- 通知详情对话框 -->
<el-dialog :title="currentNotification.title" :visible.sync="detailDialogVisible" width="600px" append-to-body>
<el-card class="notification-detail-card" v-loading="detailLoading">
<div class="notification-meta">
<div class="meta-item">
<i class="el-icon-message"></i>
<span>{{ getTypeName(currentNotification.type) }}</span>
</div>
<div class="meta-item">
<i class="el-icon-time"></i>
<span>{{ currentNotification.sendTime }}</span>
</div>
<div class="meta-item">
<i class="el-icon-user"></i>
<span>{{ currentNotification.senderName }}</span>
</div>
<div class="meta-item">
<i class="el-icon-position"></i>
<span>{{ getChannelName(currentNotification.channel) }}</span>
</div>
</div>
<div class="notification-content">
<div v-html="formatContent(currentNotification.content)"></div>
</div>
<div class="notification-actions" v-if="currentNotification.actions && currentNotification.actions.length > 0">
<div class="action-title">可执行操作:</div>
<div class="action-buttons">
<el-button
v-for="action in currentNotification.actions"
:key="action.id"
:type="action.type || 'primary'"
size="small"
@click="handleAction(action)">
{{ action.name }}
</el-button>
</div>
</div>
<div class="notification-attachments" v-if="currentNotification.attachments && currentNotification.attachments.length > 0">
<div class="attachment-title">附件:</div>
<div class="attachment-list">
<div
v-for="attachment in currentNotification.attachments"
:key="attachment.id"
class="attachment-item"
@click="downloadAttachment(attachment)">
<i class="el-icon-document"></i>
<span>{{ attachment.name }}</span>
<span class="attachment-size">({{ formatFileSize(attachment.size) }})</span>
</div>
</div>
</div>
</el-card>
<div slot="footer" class="dialog-footer">
<el-button @click="detailDialogVisible = false">关闭</el-button>
<el-button type="primary" @click="handleReply" v-if="currentNotification.canReply">回复</el-button>
</div>
</el-dialog>
<!-- 回复对话框 -->
<el-dialog title="回复通知" :visible.sync="replyDialogVisible" width="500px" append-to-body>
<el-form :model="replyForm" :rules="replyRules" ref="replyForm" label-width="80px">
<el-form-item label="回复内容" prop="content">
<el-input
type="textarea"
v-model="replyForm.content"
:rows="4"
placeholder="请输入回复内容">
</el-input>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="replyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitReply">发送</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listUnreadNotifications, markAsRead, markAllAsRead } from '@/api/notification'
// 通知列表组件
const NotificationList = {
props: {
notifications: {
type: Array,
required: true
}
},
methods: {
viewNotification(notification) {
this.$emit('view', notification)
},
markAsRead(notification) {
this.$emit('mark-read', notification)
}
},
render(h) {
return h('div', { class: 'notification-list' }, this.notifications.map(notification => {
return h('div', {
class: ['notification-item', { 'unread': !notification.read }],
key: notification.id
}, [
// 左侧图标
h('div', { class: 'notification-icon' }, [
h('el-avatar', {
props: {
size: 40,
icon: 'el-icon-message'
},
style: {
backgroundColor: this.getTypeColor(notification.type)
}
})
]),
// 中间内容
h('div', { class: 'notification-content' }, [
h('div', { class: 'notification-header' }, [
h('span', { class: 'notification-title' }, notification.title),
h('el-tag', {
props: {
size: 'mini',
type: this.getTypeTag(notification.type)
}
}, this.getTypeName(notification.type))
]),
h('div', { class: 'notification-body' }, notification.content),
h('div', { class: 'notification-footer' }, [
h('span', { class: 'notification-time' }, notification.sendTime),
h('span', { class: 'notification-channel' }, [
h('el-tag', {
props: {
size: 'mini',
type: this.getChannelTag(notification.channel)
}
}, this.getChannelName(notification.channel))
])
])
]),
// 右侧操作
h('div', { class: 'notification-actions' }, [
h('el-button', {
props: {
type: 'text',
icon: 'el-icon-view',
size: 'mini'
},
on: {
click: (e) => {
e.stopPropagation()
this.viewNotification(notification)
}
}
}, '查看'),
!notification.read ? h('el-button', {
props: {
type: 'text',
icon: 'el-icon-check',
size: 'mini'
},
on: {
click: (e) => {
e.stopPropagation()
this.markAsRead(notification)
}
}
}, '已读') : null
])
])
}))
},
methods: {
getTypeColor(type) {
switch (type) {
case 1: return '#409EFF'
case 2: return '#67C23A'
case 3: return '#E6A23C'
default: return '#909399'
}
},
getTypeTag(type) {
switch (type) {
case 1: return 'primary'
case 2: return 'success'
case 3: return 'warning'
default: return 'info'
}
},
getTypeName(type) {
switch (type) {
case 1: return '系统通知'
case 2: return '快递通知'
case 3: return '活动通知'
default: return '其他通知'
}
},
getChannelTag(channel) {
switch (channel) {
case 1: return 'primary'
case 2: return 'success'
case 3: return 'warning'
case 4: return 'danger'
default: return 'info'
}
},
getChannelName(channel) {
switch (channel) {
case 1: return '站内信'
case 2: return '短信'
case 3: return '邮件'
case 4: return '推送'
default: return '未知'
}
}
}
}
export default {
name: 'MyNotifications',
components: {
NotificationList
},
data() {
return {
// 激活的标签页
activeTab: 'all',
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
type: null,
read: null
},
// 总记录数
total: 0,
// 通知列表
notifications: [],
// 未读通知数量
unreadCount: 0,
// 详情对话框
detailDialogVisible: false,
// 详情加载状态
detailLoading: false,
// 当前查看的通知
currentNotification: {
id: undefined,
title: '',
content: '',
type: undefined,
channel: undefined,
senderName: '',
sendTime: '',
read: false,
canReply: false,
actions: [],
attachments: []
},
// 回复对话框
replyDialogVisible: false,
// 回复表单
replyForm: {
notificationId: undefined,
content: ''
},
// 回复表单校验规则
replyRules: {
content: [
{ required: true, message: '请输入回复内容', trigger: 'blur' }
]
}
}
},
computed: {
// 未读通知
unreadNotifications() {
return this.notifications.filter(item => !item.read)
},
// 系统通知
systemNotifications() {
return this.notifications.filter(item => item.type === 1)
},
// 快递通知
expressNotifications() {
return this.notifications.filter(item => item.type === 2)
},
// 活动通知
activityNotifications() {
return this.notifications.filter(item => item.type === 3)
}
},
created() {
this.getNotificationList()
},
methods: {
// 获取通知列表
getNotificationList() {
// 实际项目中的API调用
// listUnreadNotifications(this.queryParams).then(response => {
// this.notifications = response.rows
// this.total = response.total
// this.unreadCount = response.unreadCount
// })
// 模拟数据
this.notifications = [
{
id: 1,
title: '系统维护通知',
content: '系统将于2023年5月1日凌晨2点至4点进行维护,届时系统将暂停服务,请提前做好准备。',
type: 1,
channel: 1,
senderName: '系统管理员',
sendTime: '2023-04-29 10:30:00',
read: false,
canReply: false
},
{
id: 2,
title: '快递到达通知',
content: '您的快递(顺丰 SF1234567890)已到达校园快递站,取件码:8888,请及时前往领取。',
type: 2,
channel: 2,
senderName: '快递管理员',
sendTime: '2023-04-29 09:15:00',
read: false,
canReply: true,
actions: [
{ id: 1, name: '查看快递详情', type: 'primary', url: '/express/detail/123' }
]
},
{
id: 3,
title: '五一活动邀请',
content: '诚邀您参加五一劳动节文艺汇演活动,时间:2023年5月1日下午2点,地点:校园大礼堂,期待您的参与!',
type: 3,
channel: 3,
senderName: '学生会',
sendTime: '2023-04-28 16:45:00',
read: true,
canReply: true,
actions: [
{ id: 1, name: '参加', type: 'primary', action: 'join' },
{ id: 2, name: '不参加', type: 'info', action: 'decline' }
]
},
{
id: 4,
title: '教务系统更新通知',
content: '教务系统已完成版本更新,新增成绩查询、课表导出等功能,欢迎使用。',
type: 1,
channel: 4,
senderName: '教务处',
sendTime: '2023-04-28 14:20:00',
read: true,
canReply: false,
attachments: [
{ id: 1, name: '教务系统使用手册.pdf', size: 2048000 }
]
},
{
id: 5,
title: '图书馆借阅到期提醒',
content: '您借阅的《数据结构与算法》将于2023年5月5日到期,请及时归还或续借。',
type: 1,
channel: 1,
senderName: '图书馆',
sendTime: '2023-04-28 10:00:00',
read: false,
canReply: false,
actions: [
{ id: 1, name: '续借', type: 'primary', action: 'renew' }
]
}
]
this.total = this.notifications.length
this.unreadCount = this.notifications.filter(item => !item.read).length
},
// 查看通知详情
viewNotification(notification) {
this.detailLoading = true
this.currentNotification = { ...notification }
this.detailDialogVisible = true
// 如果通知未读,标记为已读
if (!notification.read) {
this.markAsRead(notification)
}
setTimeout(() => {
this.detailLoading = false
}, 500)
},
// 标记通知为已读
markAsRead(notification) {
// 实际项目中的API调用
// markAsRead(notification.id).then(response => {
// this.$message.success('已标记为已读')
// notification.read = true
// this.unreadCount = this.unreadCount - 1
// })
// 模拟标记为已读
notification.read = true
this.unreadCount = this.notifications.filter(item => !item.read).length
},
// 标记所有通知为已读
markAllAsRead() {
this.$confirm('确认将所有未读通知标记为已读吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中的API调用
// markAllAsRead().then(response => {
// this.$message.success('已全部标记为已读')
// this.notifications.forEach(item => {
// item.read = true
// })
// this.unreadCount = 0
// })
// 模拟全部标记为已读
this.notifications.forEach(item => {
item.read = true
})
this.unreadCount = 0
this.$message.success('已全部标记为已读')
}).catch(() => {})
},
// 刷新通知列表
refreshNotifications() {
this.getNotificationList()
},
// 处理标签页点击
handleTabClick(tab) {
switch (tab.name) {
case 'all':
this.queryParams.type = null
this.queryParams.read = null
break
case 'unread':
this.queryParams.type = null
this.queryParams.read = false
break
case 'system':
this.queryParams.type = 1
this.queryParams.read = null
break
case 'express':
this.queryParams.type = 2
this.queryParams.read = null
break
case 'activity':
this.queryParams.type = 3
this.queryParams.read = null
break
}
this.queryParams.pageNum = 1
this.getNotificationList()
},
// 处理通知操作
handleAction(action) {
if (action.url) {
this.$router.push(action.url)
this.detailDialogVisible = false
} else if (action.action) {
switch (action.action) {
case 'join':
this.$message.success('您已成功报名参加活动')
break
case 'decline':
this.$message.info('您已婉拒参加活动')
break
case 'renew':
this.$message.success('续借成功,新的到期日为2023年6月5日')
break
default:
break
}
}
},
// 下载附件
downloadAttachment(attachment) {
this.$message.success(`正在下载:${attachment.name}`)
},
// 回复通知
handleReply() {
this.replyForm.notificationId = this.currentNotification.id
this.replyForm.content = ''
this.replyDialogVisible = true
},
// 提交回复
submitReply() {
this.$refs.replyForm.validate(valid => {
if (valid) {
// 实际项目中的API调用
// const data = {
// notificationId: this.replyForm.notificationId,
// content: this.replyForm.content
// }
// replyNotification(data).then(response => {
// this.$message.success('回复成功')
// this.replyDialogVisible = false
// })
// 模拟回复成功
this.$message.success('回复成功')
this.replyDialogVisible = false
}
})
},
// 处理分页大小变化
handleSizeChange(size) {
this.queryParams.pageSize = size
this.getNotificationList()
},
// 处理页码变化
handleCurrentChange(page) {
this.queryParams.pageNum = page
this.getNotificationList()
},
// 格式化通知内容
formatContent(content) {
if (!content) return ''
// 将换行符转换为HTML换行
return content.replace(/\n/g, '<br>')
},
// 格式化文件大小
formatFileSize(size) {
if (size < 1024) {
return size + ' B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB'
} else if (size < 1024 * 1024 * 1024) {
return (size / (1024 * 1024)).toFixed(2) + ' MB'
} else {
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
}
},
// 获取通知类型名称
getTypeName(type) {
switch (type) {
case 1: return '系统通知'
case 2: return '快递通知'
case 3: return '活动通知'
default: return '其他通知'
}
},
// 获取通知渠道名称
getChannelName(channel) {
switch (channel) {
case 1: return '站内信'
case 2: return '短信'
case 3: return '邮件'
case 4: return '推送'
default: return '未知'
}
}
}
}
</script>
<style lang="scss" scoped>
.header-operations {
float: right;
}
.tab-content {
min-height: 400px;
padding: 10px 0;
}
.notification-list {
.notification-item {
display: flex;
padding: 15px;
border-bottom: 1px solid #ebeef5;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
&.unread {
background-color: #f0f9eb;
.notification-title {
font-weight: bold;
}
}
.notification-icon {
flex: 0 0 40px;
margin-right: 15px;
}
.notification-content {
flex: 1;
overflow: hidden;
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
.notification-title {
font-size: 14px;
color: #303133;
}
}
.notification-body {
font-size: 13px;
color: #606266;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.notification-footer {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #909399;
}
}
.notification-actions {
flex: 0 0 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-end;
}
}
}
.pagination-container {
margin-top: 20px;
text-align: center;
}
.notification-detail-card {
.notification-meta {
display: flex;
flex-wrap: wrap;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #ebeef5;
.meta-item {
flex: 0 0 50%;
margin-bottom: 10px;
i {
margin-right: 5px;
color: #909399;
}
}
}
.notification-content {
padding: 15px;
background-color: #f9f9f9;
border-radius: 4px;
min-height: 100px;
margin-bottom: 15px;
line-height: 1.6;
}
.notification-actions {
margin-bottom: 15px;
.action-title {
font-weight: bold;
margin-bottom: 10px;
}
.action-buttons {
.el-button {
margin-right: 10px;
margin-bottom: 10px;
}
}
}
.notification-attachments {
.attachment-title {
font-weight: bold;
margin-bottom: 10px;
}
.attachment-list {
.attachment-item {
display: inline-flex;
align-items: center;
padding: 5px 15px;
background-color: #f5f7fa;
border-radius: 4px;
margin-right: 10px;
margin-bottom: 10px;
cursor: pointer;
&:hover {
background-color: #ecf5ff;
}
i {
margin-right: 5px;
color: #409EFF;
}
.attachment-size {
margin-left: 5px;
color: #909399;
font-size: 12px;
}
}
}
}
}
</style>
express-ui\src\views\notification\send.vue
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>发送通知</span>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="通知类型" prop="type">
<el-select v-model="form.type" placeholder="请选择通知类型" @change="handleTypeChange">
<el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="通知模板" prop="templateId">
<el-select v-model="form.templateId" placeholder="请选择通知模板" @change="handleTemplateChange" filterable>
<el-option
v-for="item in templateOptions"
:key="item.id"
:label="item.name"
:value="item.id"
:disabled="form.type && item.type !== form.type"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="通知标题" prop="title">
<el-input v-model="form.title" placeholder="请输入通知标题" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="通知渠道" prop="channel">
<el-checkbox-group v-model="form.channel">
<el-checkbox :label="1" :disabled="selectedTemplate && !selectedTemplate.channel.includes(1)">站内信</el-checkbox>
<el-checkbox :label="2" :disabled="selectedTemplate && !selectedTemplate.channel.includes(2)">短信</el-checkbox>
<el-checkbox :label="3" :disabled="selectedTemplate && !selectedTemplate.channel.includes(3)">邮件</el-checkbox>
<el-checkbox :label="4" :disabled="selectedTemplate && !selectedTemplate.channel.includes(4)">推送</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="接收对象" prop="receiverType">
<el-radio-group v-model="form.receiverType" @change="handleReceiverTypeChange">
<el-radio :label="1">指定用户</el-radio>
<el-radio :label="2">用户组</el-radio>
<el-radio :label="3">全部用户</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="form.receiverType === 1">
<el-col :span="24">
<el-form-item label="选择用户" prop="receiverIds">
<el-select v-model="form.receiverIds" multiple filterable placeholder="请选择用户" style="width: 100%">
<el-option v-for="item in userOptions" :key="item.userId" :label="item.userName" :value="item.userId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row v-if="form.receiverType === 2">
<el-col :span="24">
<el-form-item label="选择用户组" prop="groupIds">
<el-select v-model="form.groupIds" multiple filterable placeholder="请选择用户组" style="width: 100%">
<el-option v-for="item in groupOptions" :key="item.groupId" :label="item.groupName" :value="item.groupId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="通知内容" prop="content">
<el-input v-model="form.content" type="textarea" placeholder="请输入通知内容" :rows="10" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item label="变量数据" v-if="templateVariables.length > 0">
<el-card class="variable-card">
<div v-for="(variable, index) in templateVariables" :key="index" class="variable-item">
<el-form-item :label="variable.name" :prop="'variableValues.' + index + '.value'">
<el-input v-model="variable.value" :placeholder="'请输入' + variable.name" />
</el-form-item>
</div>
</el-card>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="发送时间" prop="sendTime">
<el-radio-group v-model="form.sendTimeType" @change="handleSendTimeTypeChange">
<el-radio :label="1">立即发送</el-radio>
<el-radio :label="2">定时发送</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.sendTimeType === 2">
<el-form-item label="定时时间" prop="scheduledTime">
<el-date-picker
v-model="form.scheduledTime"
type="datetime"
placeholder="选择日期时间"
value-format="yyyy-MM-dd HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item>
<el-button type="primary" @click="submitForm">发送通知</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button type="success" @click="previewNotification">预览通知</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 预览通知对话框 -->
<el-dialog title="通知预览" :visible.sync="previewVisible" width="600px" append-to-body>
<el-card class="preview-card">
<div slot="header" class="clearfix">
<span>{{ form.title }}</span>
</div>
<div class="preview-content">
<div v-html="previewContent"></div>
</div>
<div class="preview-info">
<p><strong>发送渠道:</strong>
<el-tag v-if="form.channel.includes(1)" type="primary" class="channel-tag">站内信</el-tag>
<el-tag v-if="form.channel.includes(2)" type="success" class="channel-tag">短信</el-tag>
<el-tag v-if="form.channel.includes(3)" type="warning" class="channel-tag">邮件</el-tag>
<el-tag v-if="form.channel.includes(4)" type="danger" class="channel-tag">推送</el-tag>
</p>
<p><strong>接收对象:</strong>
<span v-if="form.receiverType === 1">指定用户 ({{ getReceiverNames() }})</span>
<span v-else-if="form.receiverType === 2">用户组 ({{ getGroupNames() }})</span>
<span v-else-if="form.receiverType === 3">全部用户</span>
</p>
<p><strong>发送时间:</strong>
<span v-if="form.sendTimeType === 1">立即发送</span>
<span v-else-if="form.sendTimeType === 2">{{ form.scheduledTime }}</span>
</p>
</div>
</el-card>
<div slot="footer" class="dialog-footer">
<el-button @click="previewVisible = false">关闭</el-button>
<el-button type="primary" @click="confirmSend">确认发送</el-button>
</div>
</el-dialog>
</el-card>
</div>
</template>
<script>
import { sendNotification, getTemplate } from '@/api/notification'
export default {
name: 'NotificationSend',
data() {
return {
// 表单参数
form: {
type: undefined,
templateId: undefined,
title: '',
content: '',
channel: [1],
receiverType: 1,
receiverIds: [],
groupIds: [],
sendTimeType: 1,
scheduledTime: undefined
},
// 表单校验
rules: {
type: [
{ required: true, message: '请选择通知类型', trigger: 'change' }
],
title: [
{ required: true, message: '请输入通知标题', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入通知内容', trigger: 'blur' }
],
channel: [
{ required: true, message: '请选择通知渠道', trigger: 'change', type: 'array' }
],
receiverIds: [
{ required: true, message: '请选择接收用户', trigger: 'change', type: 'array' }
],
groupIds: [
{ required: true, message: '请选择用户组', trigger: 'change', type: 'array' }
],
scheduledTime: [
{ required: true, message: '请选择定时发送时间', trigger: 'change' }
]
},
// 通知类型选项
typeOptions: [
{ value: '1', label: '系统通知' },
{ value: '2', label: '快递通知' },
{ value: '3', label: '活动通知' }
],
// 模板选项
templateOptions: [
{
id: 1,
name: '系统维护通知模板',
type: 1,
channel: [1, 3],
content: '尊敬的{{userName}},系统将于{{startTime}}至{{endTime}}进行系统维护,届时系统将暂停服务,请提前做好准备。'
},
{
id: 2,
name: '快递到达通知模板',
type: 2,
channel: [1, 2, 4],
content: '您好,{{userName}},您的快递({{expressCompany}} {{expressCode}})已到达校园快递站,取件码:{{pickupCode}},请及时前往领取。'
},
{
id: 3,
name: '活动邀请模板',
type: 3,
channel: [1, 3],
content: '亲爱的{{userName}},诚邀您参加{{activityName}}活动,时间:{{activityTime}},地点:{{activityLocation}},期待您的参与!'
}
],
// 用户选项
userOptions: [
{ userId: 1, userName: '管理员' },
{ userId: 2, userName: '张三' },
{ userId: 3, userName: '李四' },
{ userId: 4, userName: '王五' }
],
// 用户组选项
groupOptions: [
{ groupId: 1, groupName: '管理员组' },
{ groupId: 2, groupName: '学生组' },
{ groupId: 3, groupName: '教师组' },
{ groupId: 4, groupName: '后勤组' }
],
// 选中的模板
selectedTemplate: null,
// 模板变量
templateVariables: [],
// 预览对话框
previewVisible: false,
// 预览内容
previewContent: ''
}
},
watch: {
'form.receiverType': function(val) {
if (val === 1) {
this.rules.receiverIds = [{ required: true, message: '请选择接收用户', trigger: 'change', type: 'array' }]
this.rules.groupIds = []
} else if (val === 2) {
this.rules.groupIds = [{ required: true, message: '请选择用户组', trigger: 'change', type: 'array' }]
this.rules.receiverIds = []
} else {
this.rules.receiverIds = []
this.rules.groupIds = []
}
},
'form.sendTimeType': function(val) {
if (val === 2) {
this.rules.scheduledTime = [{ required: true, message: '请选择定时发送时间', trigger: 'change' }]
} else {
this.rules.scheduledTime = []
}
}
},
methods: {
// 处理通知类型变化
handleTypeChange(val) {
this.form.templateId = undefined
this.selectedTemplate = null
this.form.content = ''
this.templateVariables = []
},
// 处理模板变化
handleTemplateChange(val) {
const template = this.templateOptions.find(item => item.id === val)
if (template) {
this.selectedTemplate = template
this.form.content = template.content
this.form.channel = template.channel.slice(0, 1) // 默认选择第一个渠道
// 提取模板变量
this.extractTemplateVariables(template.content)
}
},
// 提取模板变量
extractTemplateVariables(content) {
const regex = /\{\{([^}]+)\}\}/g
let match
const variables = []
while ((match = regex.exec(content)) !== null) {
variables.push({
name: match[1].trim(),
placeholder: match[0],
value: ''
})
}
// 去重
this.templateVariables = variables.filter((v, i, a) => a.findIndex(t => t.name === v.name) === i)
},
// 处理接收对象类型变化
handleReceiverTypeChange(val) {
if (val === 1) {
this.form.groupIds = []
} else if (val === 2) {
this.form.receiverIds = []
} else {
this.form.receiverIds = []
this.form.groupIds = []
}
},
// 处理发送时间类型变化
handleSendTimeTypeChange(val) {
if (val === 1) {
this.form.scheduledTime = undefined
}
},
// 预览通知
previewNotification() {
this.$refs['form'].validate(valid => {
if (valid) {
let content = this.form.content
// 替换模板变量
this.templateVariables.forEach(variable => {
if (variable.value) {
const regex = new RegExp(variable.placeholder, 'g')
content = content.replace(regex, variable.value)
}
})
// 将换行符转换为HTML换行
this.previewContent = content.replace(/\n/g, '<br>')
this.previewVisible = true
}
})
},
// 获取接收者名称
getReceiverNames() {
if (!this.form.receiverIds || this.form.receiverIds.length === 0) {
return '无'
}
return this.form.receiverIds.map(id => {
const user = this.userOptions.find(item => item.userId === id)
return user ? user.userName : id
}).join(', ')
},
// 获取用户组名称
getGroupNames() {
if (!this.form.groupIds || this.form.groupIds.length === 0) {
return '无'
}
return this.form.groupIds.map(id => {
const group = this.groupOptions.find(item => item.groupId === id)
return group ? group.groupName : id
}).join(', ')
},
// 确认发送
confirmSend() {
this.submitForm()
},
// 提交表单
submitForm() {
this.$refs['form'].validate(valid => {
if (valid) {
// 构建发送参数
const sendData = {
...this.form,
// 替换模板变量
content: this.form.content
}
// 替换模板变量
this.templateVariables.forEach(variable => {
if (variable.value) {
const regex = new RegExp(variable.placeholder, 'g')
sendData.content = sendData.content.replace(regex, variable.value)
}
})
// 实际项目中的API调用
// sendNotification(sendData).then(response => {
// this.$modal.msgSuccess('发送成功')
// this.resetForm()
// this.previewVisible = false
// })
// 模拟发送成功
this.$message.success('通知发送成功')
this.resetForm()
this.previewVisible = false
}
})
},
// 重置表单
resetForm() {
this.$refs['form'].resetFields()
this.form = {
type: undefined,
templateId: undefined,
title: '',
content: '',
channel: [1],
receiverType: 1,
receiverIds: [],
groupIds: [],
sendTimeType: 1,
scheduledTime: undefined
}
this.selectedTemplate = null
this.templateVariables = []
}
}
}
</script>
<style lang="scss" scoped>
.channel-tag {
margin-right: 5px;
}
.variable-card {
margin-bottom: 20px;
.variable-item {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
}
.preview-card {
.preview-content {
padding: 15px;
min-height: 150px;
border: 1px solid #ebeef5;
border-radius: 4px;
background-color: #f9f9f9;
margin-bottom: 15px;
}
.preview-info {
padding: 10px;
border-top: 1px solid #ebeef5;
p {
margin: 5px 0;
}
}
}
</style>
express-ui\src\views\notification\settings.vue
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>通知设置</span>
</div>
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
<el-tabs v-model="activeTab">
<el-tab-pane label="通知偏好" name="preferences">
<el-divider content-position="left">通知接收设置</el-divider>
<el-form-item label="接收通知" prop="receiveNotifications">
<el-switch v-model="form.receiveNotifications"></el-switch>
<span class="form-help">关闭后将不会接收任何通知</span>
</el-form-item>
<el-form-item label="通知渠道">
<el-checkbox-group v-model="form.enabledChannels">
<el-checkbox :label="1" :disabled="!form.receiveNotifications">站内信</el-checkbox>
<el-checkbox :label="2" :disabled="!form.receiveNotifications">短信</el-checkbox>
<el-checkbox :label="3" :disabled="!form.receiveNotifications">邮件</el-checkbox>
<el-checkbox :label="4" :disabled="!form.receiveNotifications">推送</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="通知类型">
<el-checkbox-group v-model="form.enabledTypes">
<el-checkbox :label="1" :disabled="!form.receiveNotifications">系统通知</el-checkbox>
<el-checkbox :label="2" :disabled="!form.receiveNotifications">快递通知</el-checkbox>
<el-checkbox :label="3" :disabled="!form.receiveNotifications">活动通知</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-divider content-position="left">通知显示设置</el-divider>
<el-form-item label="桌面通知" prop="desktopNotifications">
<el-switch v-model="form.desktopNotifications" :disabled="!form.receiveNotifications"></el-switch>
<span class="form-help">在浏览器中显示桌面通知</span>
</el-form-item>
<el-form-item label="声音提醒" prop="soundNotifications">
<el-switch v-model="form.soundNotifications" :disabled="!form.receiveNotifications"></el-switch>
<span class="form-help">收到通知时播放提示音</span>
</el-form-item>
<el-form-item label="通知显示数量" prop="maxDisplayCount">
<el-input-number v-model="form.maxDisplayCount" :min="1" :max="50" :disabled="!form.receiveNotifications"></el-input-number>
<span class="form-help">导航栏通知中心显示的最大通知数量</span>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="时间设置" name="time">
<el-divider content-position="left">免打扰时间</el-divider>
<el-form-item label="启用免打扰" prop="enableDoNotDisturb">
<el-switch v-model="form.enableDoNotDisturb" :disabled="!form.receiveNotifications"></el-switch>
<span class="form-help">在指定时间段内不接收通知</span>
</el-form-item>
<el-form-item label="免打扰时间段" v-if="form.enableDoNotDisturb">
<el-time-picker
v-model="form.doNotDisturbStart"
format="HH:mm"
placeholder="开始时间"
:disabled="!form.receiveNotifications || !form.enableDoNotDisturb">
</el-time-picker>
<span class="time-separator">至</span>
<el-time-picker
v-model="form.doNotDisturbEnd"
format="HH:mm"
placeholder="结束时间"
:disabled="!form.receiveNotifications || !form.enableDoNotDisturb">
</el-time-picker>
</el-form-item>
<el-form-item label="免打扰日期" v-if="form.enableDoNotDisturb">
<el-checkbox-group v-model="form.doNotDisturbDays" :disabled="!form.receiveNotifications || !form.enableDoNotDisturb">
<el-checkbox :label="1">周一</el-checkbox>
<el-checkbox :label="2">周二</el-checkbox>
<el-checkbox :label="3">周三</el-checkbox>
<el-checkbox :label="4">周四</el-checkbox>
<el-checkbox :label="5">周五</el-checkbox>
<el-checkbox :label="6">周六</el-checkbox>
<el-checkbox :label="0">周日</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-divider content-position="left">定时清理</el-divider>
<el-form-item label="自动清理通知" prop="autoCleanNotifications">
<el-switch v-model="form.autoCleanNotifications"></el-switch>
<span class="form-help">定期自动清理已读通知</span>
</el-form-item>
<el-form-item label="保留时间" prop="retentionDays" v-if="form.autoCleanNotifications">
<el-input-number v-model="form.retentionDays" :min="1" :max="365"></el-input-number>
<span class="form-help">天 (超过此天数的已读通知将被自动清理)</span>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="订阅管理" name="subscriptions">
<el-divider content-position="left">通知订阅</el-divider>
<el-table :data="subscriptions" style="width: 100%" border>
<el-table-column prop="name" label="订阅类型" width="180">
<template slot-scope="scope">
<el-tag :type="getSubscriptionTagType(scope.row.type)">{{ scope.row.name }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述"></el-table-column>
<el-table-column label="订阅状态" width="100" align="center">
<template slot-scope="scope">
<el-switch
v-model="scope.row.subscribed"
:disabled="!form.receiveNotifications"
@change="handleSubscriptionChange(scope.row)">
</el-switch>
</template>
</el-table-column>
<el-table-column label="接收渠道" width="280">
<template slot-scope="scope">
<el-checkbox-group
v-model="scope.row.channels"
size="mini"
:disabled="!scope.row.subscribed || !form.receiveNotifications">
<el-checkbox :label="1">站内信</el-checkbox>
<el-checkbox :label="2">短信</el-checkbox>
<el-checkbox :label="3">邮件</el-checkbox>
<el-checkbox :label="4">推送</el-checkbox>
</el-checkbox-group>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="联系方式" name="contacts">
<el-divider content-position="left">通知接收联系方式</el-divider>
<el-alert
title="请确保您的联系方式正确,以便接收通知"
type="info"
:closable="false"
style="margin-bottom: 20px;">
</el-alert>
<el-form-item label="手机号码" prop="phoneNumber">
<el-input v-model="form.phoneNumber" placeholder="请输入手机号码">
<template slot="prepend">+86</template>
</el-input>
</el-form-item>
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入电子邮箱"></el-input>
</el-form-item>
<el-form-item label="微信绑定" prop="wechatBound">
<div v-if="form.wechatBound" class="bound-account">
<i class="el-icon-success"></i>
<span>已绑定微信账号:{{ form.wechatNickname }}</span>
<el-button type="text" @click="unbindWechat">解除绑定</el-button>
</div>
<div v-else class="unbound-account">
<i class="el-icon-warning"></i>
<span>未绑定微信账号</span>
<el-button type="primary" size="small" @click="bindWechat">绑定微信</el-button>
</div>
</el-form-item>
<el-form-item label="APP推送" prop="appBound">
<div v-if="form.appBound" class="bound-account">
<i class="el-icon-success"></i>
<span>已安装校园快递APP,设备名称:{{ form.appDeviceName }}</span>
<el-button type="text" @click="unbindApp">解除绑定</el-button>
</div>
<div v-else class="unbound-account">
<i class="el-icon-warning"></i>
<span>未安装校园快递APP</span>
<el-button type="primary" size="small" @click="downloadApp">下载APP</el-button>
</div>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item>
<el-button type="primary" @click="submitForm">保存设置</el-button>
<el-button @click="resetForm">重置</el-button>
<el-button type="success" @click="testNotification">发送测试通知</el-button>
</el-form-item>
</el-form>
<!-- 微信绑定对话框 -->
<el-dialog title="绑定微信账号" :visible.sync="wechatDialogVisible" width="400px" append-to-body>
<div class="qrcode-container">
<div class="qrcode">
<img src="https://via.placeholder.com/200" alt="微信二维码" />
</div>
<div class="qrcode-tip">请使用微信扫描二维码完成绑定</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="wechatDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmBindWechat">已完成扫码</el-button>
</div>
</el-dialog>
</el-card>
</div>
</template>
<script>
import { NotificationType, NotificationChannel } from '@/utils/notification'
export default {
name: 'NotificationSettings',
data() {
return {
// 当前激活的标签页
activeTab: 'preferences',
// 表单数据
form: {
// 通知偏好
receiveNotifications: true,
enabledChannels: [1, 3], // 默认启用站内信和邮件
enabledTypes: [1, 2, 3], // 默认启用所有类型
desktopNotifications: true,
soundNotifications: true,
maxDisplayCount: 10,
// 时间设置
enableDoNotDisturb: false,
doNotDisturbStart: null,
doNotDisturbEnd: null,
doNotDisturbDays: [],
autoCleanNotifications: true,
retentionDays: 30,
// 联系方式
phoneNumber: '',
email: '',
wechatBound: false,
wechatNickname: '',
appBound: false,
appDeviceName: ''
},
// 表单校验规则
rules: {
phoneNumber: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
retentionDays: [
{ type: 'number', min: 1, max: 365, message: '保留时间必须在1-365天之间', trigger: 'blur' }
],
maxDisplayCount: [
{ type: 'number', min: 1, max: 50, message: '显示数量必须在1-50之间', trigger: 'blur' }
]
},
// 订阅列表
subscriptions: [
{
id: 1,
type: NotificationType.SYSTEM,
name: '系统通知',
description: '系统维护、更新、安全提醒等系统相关通知',
subscribed: true,
channels: [NotificationChannel.IN_APP, NotificationChannel.EMAIL]
},
{
id: 2,
type: NotificationType.EXPRESS,
name: '快递通知',
description: '快递到达、取件提醒、催领通知等快递相关通知',
subscribed: true,
channels: [NotificationChannel.IN_APP, NotificationChannel.SMS, NotificationChannel.PUSH]
},
{
id: 3,
type: NotificationType.ACTIVITY,
name: '活动通知',
description: '校园活动、讲座、比赛等活动相关通知',
subscribed: true,
channels: [NotificationChannel.IN_APP, NotificationChannel.EMAIL]
},
{
id: 4,
type: 4,
name: '课程通知',
description: '课程变更、考试安排等课程相关通知',
subscribed: true,
channels: [NotificationChannel.IN_APP, NotificationChannel.EMAIL, NotificationChannel.SMS]
},
{
id: 5,
type: 5,
name: '公告通知',
description: '学校公告、政策变更等公告通知',
subscribed: true,
channels: [NotificationChannel.IN_APP]
}
],
// 微信绑定对话框
wechatDialogVisible: false
}
},
created() {
this.getSettings()
},
methods: {
// 获取用户通知设置
getSettings() {
// 实际项目中应该从API获取用户设置
// 这里使用模拟数据
setTimeout(() => {
this.form.phoneNumber = '13800138000'
this.form.email = 'user@example.com'
}, 500)
},
// 提交表单
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
// 实际项目中应该调用API保存设置
// saveNotificationSettings(this.form).then(response => {
// this.$message.success('设置保存成功')
// })
// 模拟保存成功
this.$message({
message: '设置保存成功',
type: 'success'
})
}
})
},
// 重置表单
resetForm() {
this.$confirm('确定要重置所有设置吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$refs.form.resetFields()
this.getSettings()
}).catch(() => {})
},
// 发送测试通知
testNotification() {
// 实际项目中应该调用API发送测试通知
// sendTestNotification().then(response => {
// this.$message.success('测试通知已发送')
// })
// 模拟发送成功
this.$message({
message: '测试通知已发送,请检查您的通知渠道',
type: 'success'
})
},
// 处理订阅状态变更
handleSubscriptionChange(subscription) {
if (!subscription.subscribed) {
subscription.channels = []
} else if (subscription.channels.length === 0) {
// 默认选择站内信
subscription.channels = [NotificationChannel.IN_APP]
}
},
// 获取订阅类型对应的标签类型
getSubscriptionTagType(type) {
switch (type) {
case NotificationType.SYSTEM:
return 'primary'
case NotificationType.EXPRESS:
return 'success'
case NotificationType.ACTIVITY:
return 'warning'
default:
return 'info'
}
},
// 绑定微信
bindWechat() {
this.wechatDialogVisible = true
},
// 确认绑定微信
confirmBindWechat() {
// 实际项目中应该调用API确认绑定状态
// confirmWechatBind().then(response => {
// this.form.wechatBound = true
// this.form.wechatNickname = response.data.nickname
// this.wechatDialogVisible = false
// this.$message.success('微信绑定成功')
// })
// 模拟绑定成功
this.form.wechatBound = true
this.form.wechatNickname = '校园快递用户'
this.wechatDialogVisible = false
this.$message({
message: '微信绑定成功',
type: 'success'
})
},
// 解除微信绑定
unbindWechat() {
this.$confirm('确定要解除微信绑定吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中应该调用API解除绑定
// unbindWechat().then(response => {
// this.form.wechatBound = false
// this.form.wechatNickname = ''
// this.$message.success('微信解绑成功')
// })
// 模拟解绑成功
this.form.wechatBound = false
this.form.wechatNickname = ''
this.$message({
message: '微信解绑成功',
type: 'success'
})
}).catch(() => {})
},
// 下载APP
downloadApp() {
// 打开下载页面
window.open('https://example.com/download', '_blank')
},
// 解除APP绑定
unbindApp() {
this.$confirm('确定要解除APP设备绑定吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 实际项目中应该调用API解除绑定
// unbindApp().then(response => {
// this.form.appBound = false
// this.form.appDeviceName = ''
// this.$message.success('APP设备解绑成功')
// })
// 模拟解绑成功
this.form.appBound = false
this.form.appDeviceName = ''
this.$message({
message: 'APP设备解绑成功',
type: 'success'
})
}).catch(() => {})
}
}
}
</script>
<style lang="scss" scoped>
.form-help {
margin-left: 10px;
color: #909399;
font-size: 14px;
}
.time-separator {
margin: 0 10px;
}
.bound-account, .unbound-account {
display: flex;
align-items: center;
i {
margin-right: 10px;
font-size: 18px;
}
.el-icon-success {
color: #67C23A;
}
.el-icon-warning {
color: #E6A23C;
}
span {
flex: 1;
}
}
.qrcode-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
.qrcode {
margin-bottom: 20px;
img {
width: 200px;
height: 200px;
}
}
.qrcode-tip {
color: #909399;
font-size: 14px;
}
}
</style>
express-ui\src\views\notification\statistics.vue