fastadmin实现站内通知功能

实现效果如下
在这里插入图片描述
在这里插入图片描述
application/admin/view/common/header.html

<style>#notificationMenu {display: none;position: absolute;top: 40px;right: 0;background: #fff;border-radius: 6px;padding: 10px 0;width: 300px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);z-index: 1000;}#notificationItem {display: block !important;}#notificationIcon {display: inline-block;}#notificationMenu .menu li {padding: 12px 15px;border-bottom: 1px solid #f0f0f0;transition: background-color 0.3s ease;cursor: pointer;}#notificationMenu .menu li:hover {background-color: #f7f7f7;}#notificationMenu .menu {max-height: 250px;overflow-y: auto;}.badge {position: relative;top: -10px;right: -10px;}
</style>
<!-- Logo -->
<a href="javascript:;" class="logo"><!-- 迷你模式下Logo的大小为50X50 --><span class="logo-mini">{$site.name|mb_substr=0,4,'utf-8'|mb_strtoupper='utf-8'|htmlentities}</span><!-- 普通模式下Logo --><span class="logo-lg">{$site.name|htmlentities}</span>
</a><!-- 顶部通栏样式 -->
<nav class="navbar navbar-static-top"><!--第一级菜单--><div id="firstnav"><!-- 边栏切换按钮--><a href="#" class="sidebar-toggle" data-toggle="offcanvas" role="button"><span class="sr-only">{:__('Toggle navigation')}</span></a><!--如果不想在顶部显示角标,则给ul加上disable-top-badge类即可--><ul class="nav nav-tabs nav-addtabs disable-top-badge hidden-xs" role="tablist">{$navlist}</ul><div class="navbar-custom-menu"><ul class="nav navbar-nav"><!-- 通知图标 --><li id="notificationItem" class="dropdown notifications-menu" style="display: block;"><a href="#" id="notificationIcon"><i class="fa fa-bell"></i><span id="notificationBadge" class="badge badge-danger">{$notice|default=0}</span></a><ul id="notificationMenu" class="dropdown-menu"><li style="padding: 10px; display: flex; justify-content: space-between; align-items: center;"><a href="javascript:void(0);" id="doNotDisturbButton" style="display: flex; align-items: center;"><i class="fa fa-bell" id="doNotDisturbIcon" style="margin-right: 5px;"></i> 免打扰</a><a href="javascript:void(0);" id="markAllReadButton"><i class="fa fa-check-circle"></i> 一键已读</a></li><li><ul id="notificationList" class="menu"></ul></li></ul></li><!-- 多语言列表 -->{if $Think.config.lang_switch_on}<li class="hidden-xs"><a href="javascript:;" class="dropdown-toggle" data-toggle="dropdown"><i class="fa fa-language"></i></a><ul class="dropdown-menu"><li class="{$config['language']=='zh-cn'?'active':''}"><a href="?ref=addtabs&lang=zh-cn">简体中文</a></li><li class="{$config['language']=='en'?'active':''}"><a href="?ref=addtabs&lang=en">English</a></li></ul></li>{/if}<!-- 账号信息下拉框 --><li class="dropdown user user-menu"><a href="#" class="dropdown-toggle" data-toggle="dropdown"><img src="{$admin.avatar|cdnurl|htmlentities}" class="user-image" alt=""><span class="hidden-xs">{$admin.nickname|htmlentities}</span></a><ul class="dropdown-menu"><!-- User image --><li class="user-header"><img src="{$admin.avatar|cdnurl|htmlentities}" class="img-circle" alt=""><p>{$admin.nickname|htmlentities}<small>{$admin.logintime|date="Y-m-d H:i:s",###}</small></p></li><li class="user-body"><div class="visible-xs"><div class="pull-left"><a href="__PUBLIC__" target="_blank"><i class="fa fa-home"style="font-size:14px;"></i>{:__('Home')}</a></div><div class="pull-right"><a href="javascript:;" data-type="all" class="wipecache"><iclass="fa fa-trash fa-fw"></i> {:__('Wipe all cache')}</a></div></div></li><!-- Menu Footer--><li class="user-footer"><div class="pull-left"><a href="general/profile" class="btn btn-primary addtabsit"><i class="fa fa-user"></i>{:__('Profile')}</a></div><div class="pull-right"><a href="{:url('index/logout')}" class="btn btn-danger"><i class="fa fa-sign-out"></i>{:__('Logout')}</a></div></li></ul></li><!-- 控制栏切换按钮 --><li class="hidden-xs"><a href="javascript:;" data-toggle="control-sidebar"><i class="fa fa-gears"></i></a></li></ul></div></div>{if $Think.config.fastadmin.multiplenav}<!--第二级菜单,只有在multiplenav开启时才显示--><div id="secondnav"><ul class="nav nav-tabs nav-addtabs disable-top-badge" role="tablist">{if $fixedmenu}<li role="presentation" id="tab_{$fixedmenu.id}" class="{:$referermenu?'':'active'}"><ahref="#con_{$fixedmenu.id}" node-id="{$fixedmenu.id}" aria-controls="{$fixedmenu.id}" role="tab"data-toggle="tab"><i class="fa fa-dashboard fa-fw"></i> <span>{$fixedmenu.title}</span> <spanclass="pull-right-container"> </span></a></li>{/if}{if $referermenu}<li role="presentation" id="tab_{$referermenu.id}" class="active"><a href="#con_{$referermenu.id}"node-id="{$referermenu.id}"aria-controls="{$referermenu.id}"role="tab" data-toggle="tab"><iclass="fa fa-list fa-fw"></i> <span>{$referermenu.title}</span> <spanclass="pull-right-container"> </span></a> <i class="close-tab fa fa-remove"></i></li>{/if}</ul></div>{/if}
</nav><script>function initNotifications() {const notificationIcon = document.getElementById("notificationIcon");const notificationMenu = document.getElementById("notificationMenu");const notificationList = document.getElementById("notificationList");const notificationBadge = document.getElementById("notificationBadge");const doNotDisturbButton = document.getElementById("doNotDisturbButton");const doNotDisturbIcon = document.getElementById("doNotDisturbIcon");let notifications = []; // 初始通知列表let doNotDisturb = localStorage.getItem("doNotDisturb") === "true";// 更新免打扰图标状态function updateDoNotDisturbIcon() {doNotDisturbIcon.className = doNotDisturb ? "fa fa-bell-slash" : "fa fa-bell";// 同步主通知图标样式const notificationIconBell = document.querySelector("#notificationIcon i");if (notificationIconBell) {notificationIconBell.className = doNotDisturb ? "fa fa-bell-slash" : "fa fa-bell";}}// 切换免打扰模式doNotDisturbButton.addEventListener("click", () => {doNotDisturb = !doNotDisturb;localStorage.setItem("doNotDisturb", doNotDisturb);updateDoNotDisturbIcon();// 同步主通知图标样式const notificationIconBell = document.querySelector("#notificationIcon i");if (notificationIconBell) {notificationIconBell.className = doNotDisturb ? "fa fa-bell-slash" : "fa fa-bell";}Toastr.info(doNotDisturb ? "已开启免打扰模式" : "已关闭免打扰模式");});// 初始化免打扰图标updateDoNotDisturbIcon();// 一键已读功能document.getElementById("markAllReadButton").addEventListener("click", markAllAsRead);//全部已读function markAllAsRead() {$.ajax({url: "notice/markAllAsRead",method: "POST",success: function (data) {if (data.code === 1) {notifications.forEach((notif) => (notif.read = 2)); // 更新本地状态updateNotifications();Toastr.success("全部已读");} else {Toastr.error(data.msg || "一键已读失败");}},error: function () {Toastr.error("网络错误,无法一键已读");},});}// 获取通知列表function fetchNotifications() {$.ajax({url: "notice/noticelist",method: "GET",success: function (response) {if (response.code === 1 && Array.isArray(response.data)) {notifications = response.data.map((item) => ({id: item.id,text: item.title,read: item.read,}));updateNotifications();} else {Toastr.error(response.msg || "获取通知失败");}},error: function () {Toastr.error("网络错误,无法获取通知");},});}// 更新通知列表和未读数function updateNotifications() {const fragment = document.createDocumentFragment();let unreadCount = 0;notificationList.innerHTML = "";notifications.forEach((notif) => {const li = document.createElement("li");li.textContent = notif.text;li.style.opacity = notif.read > 1 ? "0.5" : "1";if (notif.read == 1) unreadCount++;li.addEventListener("click", () => {notif.read = 2updateNotifications();markAsRead(notif.id); // 标记为已读});fragment.appendChild(li);});notificationList.appendChild(fragment);notificationBadge.textContent = unreadCount;}// 单个标记为已读function markAsRead(id) {$.ajax({url: "notice/markAsRead",method: "GET",data: { id: id },success: function (data) {if (data.code !== 1) {console.error(data.msg || "标记已读失败");}},error: function () {Toastr.error("网络错误,无法标记为已读");},});}// 点击通知图标时notificationIcon.addEventListener("click", () => {notificationMenu.classList.toggle("show");if (notificationMenu.classList.contains("show")) {fetchNotifications();}});// 点击外部关闭菜单document.addEventListener("click", (e) => {if (!notificationMenu.contains(e.target) && e.target !== notificationIcon) {notificationMenu.classList.remove("show");}});}// 等待 DOM 加载完成后运行document.addEventListener("DOMContentLoaded", initNotifications);
</script>

public/assets/js/backend/index.js

				var connectWebSocket = function () {var ws = new WebSocket(Config.socket_url);ws.onopen = function () {console.log("WebSocket连接已建立");};ws.onmessage = function (event) {var message = JSON.parse(event.data);//处理消息通知if (message && message.type === "ping") {// console.log(message)let doNotDisturb = localStorage.getItem("doNotDisturb");if (!doNotDisturb||doNotDisturb === 'false') {//判断是否设置免打扰模式Toastr.info(message.content || "您有一条新消息");}console.log(doNotDisturb)// 更新角标-未读数加 1const badgeElement = document.getElementById("notificationBadge");badgeElement.textContent = parseInt(badgeElement.textContent || 0) + 1;}// 处理 "init" 消息类型并发送 AJAX 请求else if (message && message.type === "init") {//Toastr.success(message.content || "Socket 链接成功");// 发送 AJAX 请求到 admin/index/bind_admin 接口,传递 client_id$.ajax({url: 'index/bind_admin',type: 'POST',data: {client_id: message.client_id},dataType: 'json',success: function (response) {if (response.code === 1) {Toastr.info(response.content || "Socket 绑定成功");} else {Toastr.error(response.content || "Socket 绑定失败");}},error: function () {Toastr.error("网络错误,绑定失败");}});}};ws.onclose = function () {console.log("WebSocket连接已关闭,正在重连...");setTimeout(connectWebSocket, 10000);  // 延迟10秒自动重连};ws.onerror = function () {Toastr.error("socket链接失败");};return ws;};// 初始化WebSocket连接var ws = connectWebSocket();

application/admin/controller/Notice.php

<?phpnamespace app\admin\controller;use app\common\controller\Backend;/*** 消息通知管理** @icon fa fa-circle-o*/
class Notice extends Backend
{/*** Notice模型对象** @var \app\admin\model\Notice*/protected $model = null;public function _initialize(){parent::_initialize();$this->model = new \app\admin\model\Notice;$this->view->assign("roleList", $this->model->getRoleList());$this->view->assign("readList", $this->model->getReadList());}//消息通知列表public function noticelist(){$notifications = $this->model->where(['role' => 1, 'uid' => $this->auth->id])->order('id desc')->select();$this->success('获取成功', '', $notifications);}//标记已读public function markAsRead(){$id = input('id');if (!$id) {$this->error('参数错误');}$info = $this->model->where(['id' => $id])->find();if (!$info) {$this->error('数据读取失败');}$info->save(['read' => 2]);$this->success('已标记为已读');}//全部已读public function markAllAsRead(){$uid = $this->auth->id;$sql = $this->model->where(['role' => 1, 'uid' => $uid])->update(['read' => 2]);if (!$sql){$this->error('无数据更新');}$this->success('已标记为已读');}
}

musql表结构如下

CREATE TABLE `fa_notice` (`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,`title` VARCHAR(255) NULL DEFAULT '0' COMMENT '消息标题' COLLATE 'utf8_general_ci',`content` TEXT NULL DEFAULT NULL COMMENT '消息内容' COLLATE 'utf8_general_ci',`uid` INT(11) NULL DEFAULT '0' COMMENT '收消息方用户id',`role` ENUM('1','2') NOT NULL COMMENT '推送:1=向后台推送,2=向用户推送' COLLATE 'utf8_general_ci',`read` ENUM('1','2') NULL DEFAULT '1' COMMENT '查看状态:1=未读,2=已读' COLLATE 'utf8_general_ci',`createtime` BIGINT(16) NULL DEFAULT NULL,`updatetime` BIGINT(16) NULL DEFAULT NULL,PRIMARY KEY (`id`) USING BTREE
)
COMMENT='消息通知表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=5
;

在这里插入图片描述
index.php映射

//获取websocket链接
$this->assignconfig('socket_url', Config::get('websocket.url')?Config::get('websocket.url'):'ws://127.0.0.1:8282');//js中Config.socket_url就可以读取值

config.php

//配置websocket地址'websocket' => ['url' => 'ws://127.0.0.1:8282',//本地测试],

Backend.php 映射未读消息数量

//渲染消息总数量--判断是否登录if ($this->auth->id){$notice_num = Db::name('notice')->where(['role'=>1,'read'=>1,'uid'=>$this->auth->id])->count();$this->assign('notice', $notice_num);}

整体实现效果逻辑
映射页面时,先查询统计消息表未读消息,让角标加载后显示未读消息数量

index.js连接websocket,实现角标未读数值更新,如果有新推送消息,角标数字+1,并弹出新消息提示框(如果设置了免打扰,免打扰的值使用localStorage.setItem存储,判断有没有该值进行是否弹框提醒。免打扰下只更新角标)
在这里插入图片描述

在这里插入图片描述
点击icon访问列表接口,渲染出未读消息,点击消息实现消息更新已读状态。点击一键已读,把当前用户的消息update一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/887399.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Axure RP教程:创建高效用户界面和交互

Axure RP是一款广受好评的软件&#xff0c;专门用于设计精致的用户界面和交互体验。这款软件提供了众多UI控件&#xff0c;并根据它们的用途进行了分类。与此同时&#xff0c;国产的即时设计软件作为Axure的替代品&#xff0c;支持在线协作和直接在浏览器中使用&#xff0c;无需…

2024-11-25 二叉树的定义

一、基本概念 1.二叉树是n(n>0)个结点的有限集合: ① 或者为空二叉树&#xff0c;即n0。 ②或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。 特点&#xff1a; ①每个结点至多只有两棵子树。 ②左右子树不能颠倒&am…

部署实战(二)--修改jar中的文件并重新打包成jar文件

一.jar文件 JAR 文件就是 Java Archive &#xff08; Java 档案文件&#xff09;&#xff0c;它是 Java 的一种文档格式JAR 文件与 ZIP 文件唯一的区别就是在 JAR 文件的内容中&#xff0c;多出了一个META-INF/MANIFEST.MF 文件META-INF/MANIFEST.MF 文件在生成 JAR 文件的时候…

对象的大小

文章目录 一、对象大小 一、对象大小 对象是类实例化出来的&#xff0c;让我们分析一下类对象中哪些成员呢&#xff1f; 类实例化出的每个对象&#xff0c;每个都有独立的数据空间&#xff0c;所以对象中肯定包含 成员变量&#xff0c;那么成员函数是否包含呢&#xff1f; 首…

01-go入门

文章目录 Go语言学习1. 简介安装windows安装linux安装编译工具安装-goland 2. 入门2.1 Helloworld注释 2.2 变量初始化打印内存地址变量交换匿名变量作用域局部变量全局变量 2.3 常量iota 2.4 数据类型布尔整数浮点类型复数字符串定义字符串字符串拼接符定义多行字符串 map数据…

数据库的联合查询

数据库的联合查询 简介为什么要使⽤联合查询多表联合查询时MYSQL内部是如何进⾏计算的构造练习案例数据案例&#xff1a;⼀个完整的联合查询的过程 内连接语法⽰例 外连接语法 ⽰例⾃连接应⽤场景示例表连接练习 ⼦查询语法单⾏⼦查询多⾏⼦查询多列⼦查询在from⼦句中使⽤⼦查…

LeetCode-632. Smallest Range Covering Elements from K Lists [C++][Java]

目录 题目描述 解题思路 【C】 【Java】 LeetCode-632. Smallest Range Covering Elements from K Listshttps://leetcode.com/problems/smallest-range-covering-elements-from-k-lists/description/ 题目描述 You have k lists of sorted integers in non-decreasing o…

UI自动化测试中公认最佳的设计模式-POM

一、概念 什么是POM&#xff1f; POM是PageObjectModule&#xff08;页面对象模式&#xff09;的缩写&#xff0c;其目的是为了Web UI测试创建对象库。在这种模式下&#xff0c;应用涉及的每一个页面应该定义为一个单独的类。类中应该包含此页面上的页面元素对象和处理这些元…

Scala文件读写——成绩分析

根据文件解决下例问题 1.读入txt文件&#xff1a;按行读入 import scala.io.Sourceobject Test文件读写_成绩分析 {def main(args: Array[String]): Unit {//1.按行读入val source Source.fromFile("score.txt")val it source.getLines()it.next()//跳过第一行wh…

C# Winform 俄罗斯方块小游戏源码

文章目录 1.设计来源俄罗斯方块小游戏讲解1.1 主界面1.2 游戏界面1.3 游戏结束界面1.4 配置游戏界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#xff1a;https:…

前端框架Vue3——响应式数据,v-on,v-show和v-if,v-for,v-bind

Vue的定义为渐进式的JavaScript框架。所谓渐进式&#xff0c;是指其被设计 为可以自底向上逐层应用。我们可以只使用Vue框架中提供的某层的功 能&#xff0c;也可以与其他第三方库整合使用。当然&#xff0c;Vue本身也提供了完整的 工具链&#xff0c;使用其全套功能进行项目的…

实验二 系统响应及系统稳定性

实验目的 &#xff08;1&#xff09;学会运用Matlab 求解离散时间系统的零状态响应&#xff1b; &#xff08;2&#xff09;学会运用Matlab 求解离散时间系统的单位取样响应&#xff1b; &#xff08;3&#xff09;学会运用Matlab 求解离散时间系统的卷积和。 实验原理及实…

.NET Core发布网站报错 HTTP Error 500.31

报错如图&#xff1a; 解决办法&#xff1a; 打开任务管理器》》服务》》找到这仨服务&#xff0c;右键启动即可&#xff0c;如果已经启动了就重启&#xff1a;

麒麟安全增强-kysec

DAC: 自主访问控制是linux下默认的接入控制机制,通过对资源读、写、执行操作,保证系统安全 MAC:安全接入控制机制,由操作系统约束的访问控制,默认情况下,MAC不允许任何访问,用户可以自定义策略规则制定允许什么 ,从而避免很多攻击。 MAC强制访问控制常见的实现方式:…

Otter 安装流程

优质博文&#xff1a;IT-BLOG-CN 一、背景 随着公司的发展&#xff0c;订单库的数据目前已达到千万级别&#xff0c;需要进行分表分库&#xff0c;就需要对数据进行迁移&#xff0c;我们使用了otter&#xff0c;这里简单整理下&#xff0c;otter 的安装过程&#xff0c;希望对…

如何解决Jupyter command `jupyter-contrib` not found.

目录 (base) C:\Users\hello>pip show jupyter_contrib_nbextensions Name: jupyter-contrib-nbextensions Version: 0.7.0 Summary: A collection of Jupyter nbextensions. Home-page: https://github.com/ipython-contrib/jupyter_contrib_nbextensions.git Author: ipyt…

Gitee markdown 使用方法(持续更新)

IPKISS 获取仿真器件的名称 引言正文标题换行第一种------在行末尾手动键入两个空格第二种------额外换行一次&#xff0c;即两行中间留一个空行 缩进与反缩进代码块行内代码添加图片添加超链接 加粗&#xff0c;倾斜&#xff0c;加粗倾斜 引言 有些保密性的文件或者教程&…

Adobe Illustrator 2024 安装教程与下载分享

介绍一下 下载直接看文章末尾 Adobe Illustrator 是一款由Adobe Systems开发的矢量图形编辑软件。它广泛应用于创建和编辑矢量图形、插图、徽标、图标、排版和广告等领域。以下是Adobe Illustrator的一些主要特点和功能&#xff1a; 矢量绘图&#xff1a;Illustrator使用矢量…

golang学习5

为结构体添加方法 异常处理过程

分布式光伏与储能协调控制策略的应用分析

安科瑞汪洋/汪小姐/汪女士---Acrelwy 摘 要&#xff1a;针对光伏发电的随机性、波动性、间歇性特征,研发了分布式光伏与储能协调控制策略,并在镇江地调开展分布式电源光储协控试点应用&#xff0c;开展光储协调控制策略研究&#xff0c;实时采集分布式光伏电站、储能电站、试点…