Java全栈项目--校园快递管理与配送系统(4)
源代码续
/*** 通知工具类*/// 通知类型常量
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 = /\{\{([^}]+)\}\}/glet matchconst 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 contentlet result = contentvariables.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 0return 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 0return 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 0return 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.idif (id) {this.loading = true// 模拟数据,实际项目中应该调用APIsetTimeout(() => {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) {// 模拟数据,实际项目中应该调用APIthis.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-inputplaceholder="搜索通知内容"v-model="searchQuery"clearablestyle="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-pickerv-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-tablev-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-buttonv-if="!scope.row.read"size="mini"type="success"@click="markAsRead(scope.row)"icon="el-icon-check">标为已读</el-button><el-buttonsize="mini"type="primary"@click="viewDetail(scope.row)"icon="el-icon-view">查看</el-button><el-buttonsize="mini"type="danger"@click="deleteNotification(scope.row)"icon="el-icon-delete">删除</el-button></template></el-table-column></el-table><!-- 分页 --><div class="pagination-container"><el-paginationbackground@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 = 85this.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 = 1this.fetchNotifications()},// 处理清除搜索handleClear() {this.searchQuery = ''this.handleSearch()},// 处理筛选条件变化handleFilterChange() {this.pagination.currentPage = 1this.fetchNotifications()},// 处理页码变化handleCurrentChange(page) {this.pagination.currentPage = pagethis.fetchNotifications()},// 处理每页显示数量变化handleSizeChange(size) {this.pagination.pageSize = sizethis.pagination.currentPage = 1this.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 = truenotification.readTime = new Date().toISOString()if (showMessage) {this.$message({message: '已标记为已读',type: 'success'})}},// 在对话框中标记为已读markAsReadInDialog() {this.markAsRead(this.currentNotification)this.currentNotification.read = truethis.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-pickerv-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><!-- 分页 --><paginationv-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// 模拟数据,实际项目中应该调用APIthis.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.lengththis.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 = 1this.getList()},/** 重置按钮操作 */resetQuery() {this.dateRange = []this.resetForm('queryForm')this.handleQuery()},/** 新增按钮操作 */handleAdd() {this.reset()this.open = truethis.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 = truethis.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 = falsethis.getList()} else {// addNotification(this.form).then(response => {// this.$modal.msgSuccess('新增成功')// this.open = false// this.getList()// })this.$message.success('新增成功')this.open = falsethis.getList()}}})},/** 删除按钮操作 */handleDelete(row) {const ids = row.id || this.idsthis.$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 !== 1this.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 = falsethis.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-paginationbackground@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.lengththis.unreadCount = this.notifications.filter(item => !item.read).length},// 查看通知详情viewNotification(notification) {this.detailLoading = truethis.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 = truethis.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 = 0this.$message.success('已全部标记为已读')}).catch(() => {})},// 刷新通知列表refreshNotifications() {this.getNotificationList()},// 处理标签页点击handleTabClick(tab) {switch (tab.name) {case 'all':this.queryParams.type = nullthis.queryParams.read = nullbreakcase 'unread':this.queryParams.type = nullthis.queryParams.read = falsebreakcase 'system':this.queryParams.type = 1this.queryParams.read = nullbreakcase 'express':this.queryParams.type = 2this.queryParams.read = nullbreakcase 'activity':this.queryParams.type = 3this.queryParams.read = nullbreak}this.queryParams.pageNum = 1this.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('您已成功报名参加活动')breakcase 'decline':this.$message.info('您已婉拒参加活动')breakcase 'renew':this.$message.success('续借成功,新的到期日为2023年6月5日')breakdefault:break}}},// 下载附件downloadAttachment(attachment) {this.$message.success(`正在下载:${attachment.name}`)},// 回复通知handleReply() {this.replyForm.notificationId = this.currentNotification.idthis.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 = sizethis.getNotificationList()},// 处理页码变化handleCurrentChange(page) {this.queryParams.pageNum = pagethis.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-pickerv-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 = undefinedthis.selectedTemplate = nullthis.form.content = ''this.templateVariables = []},// 处理模板变化handleTemplateChange(val) {const template = this.templateOptions.find(item => item.id === val)if (template) {this.selectedTemplate = templatethis.form.content = template.contentthis.form.channel = template.channel.slice(0, 1) // 默认选择第一个渠道// 提取模板变量this.extractTemplateVariables(template.content)}},// 提取模板变量extractTemplateVariables(content) {const regex = /\{\{([^}]+)\}\}/glet matchconst 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 = nullthis.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-pickerv-model="form.doNotDisturbStart"format="HH:mm"placeholder="开始时间":disabled="!form.receiveNotifications || !form.enableDoNotDisturb"></el-time-picker><span class="time-separator">至</span><el-time-pickerv-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-switchv-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-alerttitle="请确保您的联系方式正确,以便接收通知"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 = truethis.form.wechatNickname = '校园快递用户'this.wechatDialogVisible = falsethis.$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 = falsethis.form.wechatNickname = ''this.$message({message: '微信解绑成功',type: 'success'})}).catch(() => {})},// 下载APPdownloadApp() {// 打开下载页面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 = falsethis.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
相关文章:
Java全栈项目--校园快递管理与配送系统(4)
源代码续 /*** 通知工具类*/// 通知类型常量 export const NotificationType {SYSTEM: 1,EXPRESS: 2,ACTIVITY: 3 }// 通知类型名称映射 export const NotificationTypeNames {[NotificationType.SYSTEM]: 系统通知,[NotificationType.EXPRESS]: 快递通知,[NotificationType…...
c语言练习一
1、统计二进制数中1的个数 #include <stdio.h>int main(void) {int count 0; //统计1出现次数 int x 0b1011;while(x){count ;//x 0b1011 > x-1 0b1010 x-1,将x从右往左数遇到第一个1变成0,左边全部变为1,右边不变 //x&x-1 1010 …...
Scala安装
Spark安装 Spark的Local模式仅需要单个虚拟机节点即可,无需启动hadoop集群。实验步骤如下: 将spark的安装包上传到虚拟机node01中(建议路径:/opt/software/spark)并解压缩文件。将解压文件夹重命名为spark-local 解…...
爱普生RTC模块RA8804CE在ADAS域控制器的应用
在汽车智能化、自动化飞速发展的时代,ADAS(高级驾驶辅助系统)的多传感器融合与实时决策高度依赖精准的时间基准。毫秒级的时间偏差可能导致传感器数据错位,直接影响行车安全。爱普生RA8804CE实时时钟模块凭借其内置的32.768 kHz晶…...
开箱即用!推荐一款Python开源项目:DashGo,支持定制改造为测试平台!
大家好,我是狂师。 市面上的开源后台管理系统项目层出不穷,对应所使用到的技术栈也不尽相同。 今天给大家推荐一款开源后台管理系统: DashGo,不仅部署起来非常的简单,而且它是基于Python技术栈实现的,使得基于它进行…...
C++使用WebView2控件,通过IPC通信与Javascript交互
引言 在现代桌面应用程序开发中,Web技术与原生应用的融合变得越来越普遍。Microsoft的WebView2控件为C开发者提供了一个强大的工具,使他们能够在桌面应用中嵌入基于Chromium的Web浏览器引擎。本文将详细介绍如何在C应用程序中使用WebView2控件ÿ…...
算法中Hash备胎——LRU的设计与实现
核心内容1.理解LRU的原理2.理解LRU是如何实现的3.能够通过代码实现LRU 缓存是应用软件的必备功能之一,在操作系统、Java里的Spring、mybatis、redis、mysql等软件中都有自己的内部缓存模块,而缓存是如何实现的? 在操作系统教科书里我们知道…...
Profinet邂逅ModbusRTU:印刷厂有网关“一键打通”通信链路
Profinet邂逅ModbusRTU:印刷厂有网关“一键打通”通信链路 在现代化印刷厂的生产线上,高效稳定的设备通信是保障印刷质量和生产效率的关键。某印刷厂的印刷机控制系统采用了西门子PLC进行自动化控制,同时还有众多基于ModbusRTU协议的传感器和…...
Spring中使用Kafka的详细配置,以及如何集成 KRaft 模式的 Kafka
在 Spring 中使用 Apache Kafka 的配置主要涉及 Spring Boot Starter for Kafka,而开启 KRaft 模式(Kafka 的元数据管理新模式,替代 ZooKeeper)需要特定的 Kafka Broker 配置。以下是详细步骤: 一、Spring 中集成 …...
C++之继承
本节我们将要学习C作为面向对象语言的三大特性之一的继承。 前言 一、继承的概念 二、继承的定义 2.1 定义格式 2.2 继承基类成员访问方式的变化 2.3 继承类模板 三、基类和派生类之间的转换 四、继承中的作用域 五、派生类的默认成员函数 六、实现一个不能被继承的类 七、继承…...
服务器申请 SSL 证书注意事项
一、确认证书类型 基础域名型(DV):这类证书验证速度最快,通常只需验证域名所有权,几分钟到几小时即可颁发。适用于个人博客、小型企业展示网站等对安全性要求不是顶级严苛,且急需启用 HTTPS 的场景。但它仅…...
【资料分享】全志T536(异构多核ARMCortex-A55+玄铁E907 RISC-V)工业核心板说明书
核心板简介 创龙科技SOM-TLT536是一款基于全志科技T536MX-CEN2/T536MX-CXX四核ARM Cortex-A55 +...
博途 TIA Portal之1200做从站与调试助手的TCP通讯
其实,1200做从站与调试助手做TCP通讯很简单,只需要在组态时把“主动建立连接”放在对侧即可。但是我们还是要从头讲起,以方便没有基础的朋友能直接上手操作。 1、硬件准备 1200PLC一台,带调试助手的PC机一台,调试助手…...
移动端六大语言速记:第9部分 - 并发与多线程
移动端六大语言速记:第9部分 - 并发与多线程 本文将对比Java、Kotlin、Flutter(Dart)、Python、ArkTS和Swift这六种移动端开发语言在并发与多线程方面的特性,帮助开发者理解和掌握各语言的并发编程机制。 9. 并发与多线程 9.1 线程与进程 各语言线程与进程的创建和管理方…...
基于大模型的ALS预测与手术优化系统技术方案
目录 技术方案文档:基于大模型的ALS预测与手术优化系统1. 数据预处理与特征工程模块流程图伪代码2. 多模态融合预测模型模型架构图伪代码3. 术中实时监测与动态干预系统系统流程图伪代码4. 统计验证与可解释性模块验证流程图伪代码示例(SHAP分析)5. 健康教育与交互系统系统架…...
【Vue3知识】组件间通信的方式
组件间通信的方式 概述**1. 父子组件通信****父组件向子组件传递数据(Props)****子组件向父组件发送事件(自定义事件)** **2. 兄弟组件通信****通过父组件中转****使用全局状态管理(如 Pinia 或 Vuex)** **…...
【数据挖掘】岭回归(Ridge Regression)和线性回归(Linear Regression)对比实验
这是一个非常实用的 岭回归(Ridge Regression)和线性回归(Linear Regression)对比实验,使用了 scikit-learn 中的 California Housing 数据集 来预测房价。 📦 第一步:导入必要的库 import num…...
RuntimeError: Error(s) in loading state_dict for ChartParser
一 bug错误 最近使用千问大模型有一个bug,报错信息如下 raise RuntimeError(Error(s) in loading state_dict for {}:\n\t{}.format( RuntimeError: Error(s) in loading state_dict for ChartParser:Unexpected key(s) in state_dict: "pretrained_model.em…...
汽车无钥匙启动125KHz低频发射天线工作原理
汽车智能钥匙低频天线是无钥匙进入(PE)及无钥匙启动(PS)系统的一部分,主要负责发送低频信号,探测智能钥匙与各低频天线间的相对位置,判断车内是否存在智能钥匙。 支持PEPS系统实现便捷操作 无…...
【Docker基础-镜像】--查阅笔记2
目录 Docker镜像概述base镜像镜像的分层结构镜像的理解镜像的构建docker commit 制作镜像DockerfileDockerfile 指令FROMLABELRUNARGENVADDCOPYWORKDIRUSERVOLUMEEXPOSECMD 和 ENTRYPOINT Docker镜像概述 镜像是Docker容器的基石,容器是镜像的运行实例,…...
LeetCode 第47题:旋转数组
LeetCode 第47题:旋转数组 题目描述 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例1: 输入…...
数据库管理工具实战:IDEA 与 DBeaver 连接 TDengine(二)
五、DBeaver 连接 TDengine 实战 5.1 安装 DBeaver 下载安装包:访问 DBeaver 官方网站(https://dbeaver.io/download/ ),根据你的操作系统选择合适的安装包。如果是 Windows 系统,下载.exe 格式的安装文件࿱…...
4S店汽车维修保养管理系统 (源码+lw+部署文档+讲解),源码可白嫖!
摘要 二十一世纪我们的社会进入了信息时代,信息管理系统的建立,大大提高了人们信息化水平。传统的管理方式已经与当今4S店汽车维修保养管理系统的业务需求不相适应,也与4S店汽车维修保养管理系统化建设的发展趋势不相适应。本文针对这一需求设计并实现了…...
【Mysql】主从复制部署(保姆级)
本次部署用到三台Ubuntu虚拟机(一主两从): Master服务器:192.168.166.107 Slave1服务器:192.168.166.101 Slave2服务器:192.168.166.103 一、部署思路 首先我们要先捋清主从复制的部署思路…...
华为AR1200密码忘记
1、通过Console口连接设备并重启设备。在设备启动过程中,看到提示信息“Press CtrlB to break auto startup...”时,在三秒内按下CtrlB,输入BootLoader密码后,默认密码:Adminhuawei ,进入BootLoader主菜单…...
高级java每日一道面试题-2025年3月26日-微服务篇[Nacos篇]-在Spring Cloud项目中如何集成Nacos?
如果有遗漏,评论区告诉我进行补充 面试官: 在Spring Cloud项目中如何集成Nacos? 我回答: 在Spring Cloud项目中集成Nacos,可以充分利用Nacos作为服务注册与发现中心以及配置管理中心的功能。以下是详细的步骤和说明,帮助你完成这一集成过程…...
YOLO-LLTS:低光照实时交通标志检测算法详解
论文地址:https://arxiv.org/pdf/2503.13883 目录 一、论文概述 1.1 研究背景 1.2 论文结构 二、核心创新点 2.1 CNTSSS数据集 2.2 HRFM-TOD模块 2.3 MFIA模块 2.4 PGFE模块 三、实验与结果 3.1 实验设置 3.2 性能对比 编辑3.3 消融实验 四、代码复现建议 4.…...
golang 性能优化分析工具 pprof
pprof简介 pprof 是 Go 语言标准库提供的一个强大的性能分析工具,它能帮助开发者深入了解程序的运行时行为,找出性能瓶颈,进而对代码进行优化。下面从多个方面对 pprof 进行详细介绍: 主要功能 CPU 性能分析:能够记…...
机器学习 Day09 线性回归
1.线性回归简介 线性回归知识讲解 定义与公式 定义:线性回归是利用回归方程(函数)对自变量(特征值)和因变量(目标值)之间关系进行建模的分析方式 。自变量只有一个时是单变量回归,…...
2025高频面试算法总结篇【字符串】
文章目录 直接刷题链接直达如何找出一个字符串中的最大不重复子串给定一个数,删除K位得到最大值字符串的排列至少有K个重复字符的最长子串 直接刷题链接直达 如何找出一个字符串中的最大不重复子串 滑动窗口 --> 滑动窗口直到最后一个元素,每当碰到重…...
JavaScript性能优化(上)
1. 减少 DOM 操作 减少 DOM 操作是优化 JavaScript 性能的重要方法,因为频繁的 DOM 操作会导致浏览器重绘和重排,从而影响性能。以下是一些具体的策略和技术,可以帮助有效减少 DOM 操作: 1.1. 批量更新 DOM 亲切与母体ÿ…...
数据结构与算法——链表OJ题详解(1)
文章目录 一、前言二、OJ题分享2.1移除链表元素——非val尾插法2.2反转链表2.2.1头插法2.2.2三指针法 2.3链表的中间结点——快慢指针法2.4合并两个有序链表2.4.1空链表法2.4.2非空链表法 2.5链表的回文结构2.5.1投机取巧数组法2.5.2反转链表法 三、总结 一、前言 前几天博主已…...
sedex认证2025年变化重点
近日,SEDEX突然宣布:2025年7月1日起,全通知审核正式退出历史舞台,取而代之的是至少3周窗口期的半通知突击审核。这场被业内称为“供应链透明化革命”的调整,或将重塑全球工厂合规生态。 三大变化划重点: 1…...
Scala课后总结(8)
集合计算高级函数 过滤(filter) 从集合里挑出符合特定条件元素组成新集合 。比如从整数集合里选出偶数, list.filter(x > x % 2 0) ,就是筛选出能被2整除的元素。 转化/映射(map) 对集合每个元素应…...
老硬件也能运行的Win11 IoT LTSC (OEM)物联网版
#记录工作 Windows 11 IoT Enterprise LTSC 2024 属于物联网相关的版本。 Windows 11 IoT Enterprise 是为物联网设备和场景设计的操作系统版本。它通常针对特定的工业控制、智能设备等物联网应用进行了优化和定制,以满足这些领域对稳定性、安全性和长期支持的需求…...
蓝桥杯冲刺题单--二分
二分 知识点 二分: 1.序列二分:在序列中查找(不怎么考,会比较难?) 序列二分应用的序列必须是递增或递减,但可以非严格 只要r是mid-1,就对应mid(lr1)/2 2.答…...
计网 2025/4/8
CDMA? CRC循环冗余检验 PPP协议的帧格式 字节填充(异步传输、7E->7D5E)零比特填充(同步传输、确保不会出现连续6个1) CSMA/CD协议 多点接入载波监听碰撞检测 一些概念: 争用期 一些公式: 最短有效帧…...
java设计模式-工厂模式
工厂模式 简单工厂模式 请看类: org.xwb.springcloud.factory.simple.PizzaStore 1、简单工厂模式是属于创建型模式,是工厂模式的一种,简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实力。简单来工厂模式就是工厂模式家族中最简单最…...
2025年客运从业资格证备考刷题题库
题库中通常包含大量的题目,以全面覆盖考试的知识点。通过做大量的题目,考生可以熟悉各种考试题型和命题方式,提高答题的速度和准确性,同时也能发现自己在知识掌握上的薄弱环节,有针对性地进行复习和强化训练。 1、驾驶…...
Zephyr、FreeRTOS、RT-Thread 任务创建对比分析
一、任务模型与核心概念 特性ZephyrFreeRTOSRT-Thread任务术语线程(Thread)任务(Task)线程(Thread)执行单元线程(单地址空间)任务(共享内存空间)线程&#x…...
RK-realtime Linux
rk3562实时性数据:最大76us rk3568实时性数据:最大126us rk3588实时性数据:最大30us 注意事项 (1)RK3568 需要使用RT版本的BL31,实时性能更好 a)rkbin需要更新到最新,且包含这个补丁:...
Ubuntu 22 Linux上部署DeepSeek+RAG知识库操作详解(Dify方式)之1
一、安装Docker 1. 更新你的包索引 首先,确保你的包列表是最新的。打开终端并运行以下命令: sudo apt update 2. 安装必要的依赖项 安装Docker之前,你需要安装一些必要的依赖项。运行以下命令来安装它们: sudo apt install apt…...
将飞帆制作的网页作为 div 集成到自己的网页中
并且自己的网页可以和飞帆中的控件相互调用函数。效果: 上链接 将飞帆制作的网页作为 div 集成到自己的网页中 - 文贝 进入可以复制、运行代码...
【C++游戏引擎开发】《几何算法》(3)AABB/OBB碰撞检测
一、AABB(轴对齐包围盒) 1.1 定义 最小点: m i n = ( x min , y min , z min ) \mathbf{min} = (x_{\text{min}}, y_{\text{min}}, z_{\text{min}}) min=(xmin,ymin,zmin)最大点: m a x = ( x max , y max , z max ) \mathbf{max} = (x_{\text{max}}, y_{\text{…...
基于人工智能的高中教育评价体系重构研究
基于人工智能的高中教育评价体系重构研究 一、引言 1.1 研究背景 在科技飞速发展的当下,人工智能技术已广泛渗透至各个领域,教育领域亦不例外。人工智能凭借其强大的数据处理能力、智能分析能力和个性化服务能力,为教育评价体系的创新与发…...
【C++游戏引擎开发】数学计算库GLM(线性代数)、CGAL(几何计算)的安装与使用指南
写在前面 两天都没手搓实现可用的凸包生成算法相关的代码,自觉无法手搓相关数学库,遂改为使用成熟数学库。 一、GLM库安装与介绍 1.1 vcpkg安装GLM 跨平台C包管理利器vcpkg完全指南 在PowerShell中执行命令: vcpkg install glm# 集成到系…...
Python 字典和集合(常见的映射方法)
本章内容的大纲如下: 常见的字典方法 如何处理查找不到的键 标准库中 dict 类型的变种set 和 frozenset 类型 散列表的工作原理 散列表带来的潜在影响(什么样的数据类型可作为键、不可预知的 顺序,等等) 常见的映射方法 映射类型…...
Qt 自带的QSqlDatabase 模块中使用的 SQLite 和 SQLite 官方提供的 C 语言版本(sqlite.org)对比
Qt 自带的 QSqlDatabase 模块中使用的 SQLite 和 SQLite 官方提供的 C 语言版本(sqlite.org)在核心功能上是相同的,但它们在集成方式、API 封装、功能支持以及版本更新上存在一些区别。以下是主要区别: 1. 核心 SQLite 引擎 Qt 的…...
按键长按代码
这些代码都存放在定时器中断中。中断为100ms中断一次。 数据判断,看的懂就看吧...
zk源码—3.单机和集群通信原理一
大纲 1.单机版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 2.集群版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 (3)Leader选举阶段 (4)Leader和Follower启动阶段 1.单机版的zk服务端的启动过程 (1)预启动阶段 (2)初始化阶段 单机版zk服务端的启动&…...