当前位置: 首页 > article >正文

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"><i
                                            class="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'}"><a
                    href="#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> <span
                    class="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"><i
                    class="fa fa-list fa-fw"></i> <span>{$referermenu.title}</span> <span
                    class="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 = 2
                    updateNotifications();
                    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)

                        // 更新角标-未读数加 1
                        const 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

<?php

namespace 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.kler.cn/a/406651.html

相关文章:

  • 【泥石流;风险;脆弱性;风险评估;川藏公路北线|论文解读4】川藏高速公路北线泥石流风险评估
  • 利用 GitHub 和 Hexo 搭建个人博客【保姆教程】
  • 小鹏汽车智慧材料数据库系统项目总成数据同步
  • 【C#设计模式(14)——责任链模式( Chain-of-responsibility Pattern)】
  • 基于Java Springboot高考志愿填报辅助系统
  • 设计模式之创建模式篇
  • [数组双指针] 0167. 两数之和 II - 输入有序数组
  • 为什么芯麦的 GC4931P 可以替代A4931/Allegro 的深度对比介绍
  • Android开发实战班-Android App 的启动过程
  • 分布式系统稳定性建设-性能优化篇
  • 【大数据学习 | Spark-Core】yarn-client与yarn-cluster的区别
  • Oracle 19c Rac + ADG搭建(源库:RAC,目标库FS)
  • 迈向AI驱动的数据新时代:探索SQL Server 2025的全新向量数据库
  • 一文说清:C和C++混合编程
  • VTK知识学习(12)- 读取PNG图像
  • 深入探索JMeter bin目录中的Properties文件:优化性能测试的关键
  • 【功能实现】bilibili顶部鼠标跟随效果怎么实现?
  • Python +Pyqt5 简单视频爬取学习及工具实现(二)
  • 5.STM32之通信接口《精讲》之USART通信---实验串口接收程序
  • 关于汽车多核架构
  • 算法专题十一: 基础递归
  • Tomcat 任意写入文件漏洞(CVE-2017-12615)
  • docker镜像源配置、换源、dockerhub国内镜像最新可用加速源(仓库)
  • 10 分钟,教你如何用 LLama-Factory 训练和微调 LLama3 模型
  • 计算机网络(14)ip地址超详解
  • Vision-Language Models for Vision Tasks: A Survey 论文解读