PHP项目公告与弹窗管理

wen PHP项目 2

本文目录导读:

PHP项目公告与弹窗管理

  1. 系统架构设计
  2. 使用说明

我来为您提供一个PHP项目中的公告与弹窗管理系统设计方案。

系统架构设计

数据库设计

-- 公告表
CREATE TABLE `announcements` (
    `id` int(11) NOT NULL AUTO_INCREMENT, varchar(200) NOT NULL COMMENT '公告标题',
    `content` text NOT NULL COMMENT '公告内容',
    `type` tinyint(1) DEFAULT '1' COMMENT '类型:1普通公告 2重要公告 3紧急公告',
    `status` tinyint(1) DEFAULT '1' COMMENT '状态:0草稿 1发布 2已下线',
    `priority` int(11) DEFAULT '0' COMMENT '优先级',
    `is_top` tinyint(1) DEFAULT '0' COMMENT '是否置顶',
    `start_time` datetime DEFAULT NULL COMMENT '开始时间',
    `end_time` datetime DEFAULT NULL COMMENT '结束时间',
    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
    `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_status` (`status`),
    KEY `idx_type` (`type`),
    KEY `idx_time` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 弹窗表
CREATE TABLE `popups` (
    `id` int(11) NOT NULL AUTO_INCREMENT, varchar(200) NOT NULL COMMENT '弹窗标题',
    `content` text COMMENT '弹窗内容',
    `image_url` varchar(500) DEFAULT NULL COMMENT '弹窗图片',
    `link_url` varchar(500) DEFAULT NULL COMMENT '点击链接',
    `type` tinyint(1) DEFAULT '1' COMMENT '类型:1页面弹窗 2悬浮窗 3引导弹窗',
    `position` varchar(50) DEFAULT 'center' COMMENT '位置:center/top/left/right/bottom',
    `width` int(11) DEFAULT '400' COMMENT '宽度',
    `height` int(11) DEFAULT '300' COMMENT '高度',
    `show_type` tinyint(1) DEFAULT '1' COMMENT '显示方式:1一次性 2每次访问 3每天一次',
    `status` tinyint(1) DEFAULT '1' COMMENT '状态:0禁用 1启用',
    `start_time` datetime DEFAULT NULL COMMENT '开始时间',
    `end_time` datetime DEFAULT NULL COMMENT '结束时间',
    `created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
    `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`),
    KEY `idx_status` (`status`),
    KEY `idx_time` (`start_time`,`end_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户阅读记录表
CREATE TABLE `user_read_records` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `user_id` int(11) NOT NULL,
    `record_type` tinyint(1) NOT NULL COMMENT '类型:1公告 2弹窗',
    `record_id` int(11) NOT NULL COMMENT '对应ID',
    `read_time` datetime NOT NULL COMMENT '阅读时间',
    `read_count` int(11) DEFAULT '1 COMMENT '阅读次数',
    PRIMARY KEY (`id`),
    KEY `idx_user_type` (`user_id`,`record_type`),
    KEY `idx_record` (`record_type`,`record_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心PHP代码实现

<?php
// PopupManager.php
class PopupManager {
    private $db;
    private $userId;
    public function __construct($db, $userId = null) {
        $this->db = $db;
        $this->userId = $userId;
    }
    /**
     * 获取当前有效的公告列表
     */
    public function getActiveAnnouncements($limit = 10) {
        $now = date('Y-m-d H:i:s');
        $sql = "SELECT * FROM announcements 
                WHERE status = 1 
                AND (start_time IS NULL OR start_time <= :now)
                AND (end_time IS NULL OR end_time >= :now)
                ORDER BY is_top DESC, priority DESC, created_at DESC 
                LIMIT :limit";
        $stmt = $this->db->prepare($sql);
        $stmt->bindParam(':now', $now);
        $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 获取当前有效的弹窗
     */
    public function getActivePopups($page = '') {
        $now = date('Y-m-d H:i:s');
        $sql = "SELECT * FROM popups 
                WHERE status = 1 
                AND (start_time IS NULL OR start_time <= :now)
                AND (end_time IS NULL OR end_time >= :now)
                AND (target_page IS NULL OR target_page = '' OR target_page = :page)
                ORDER BY priority DESC, created_at DESC";
        $stmt = $this->db->prepare($sql);
        $stmt->bindParam(':now', $now);
        $stmt->bindParam(':page', $page);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 检查用户是否已读公告
     */
    public function hasReadAnnouncement($announcementId) {
        if (!$this->userId) return false;
        $sql = "SELECT COUNT(*) FROM user_read_records 
                WHERE user_id = :user_id 
                AND record_type = 1 
                AND record_id = :record_id";
        $stmt = $this->db->prepare($sql);
        $stmt->bindParam(':user_id', $this->userId);
        $stmt->bindParam(':record_id', $announcementId);
        $stmt->execute();
        return $stmt->fetchColumn() > 0;
    }
    /**
     * 标记已读
     */
    public function markAsRead($recordType, $recordId) {
        if (!$this->userId) return false;
        // 检查是否已存在记录
        $existingSql = "SELECT id, read_count FROM user_read_records 
                        WHERE user_id = :user_id 
                        AND record_type = :record_type 
                        AND record_id = :record_id";
        $stmt = $this->db->prepare($existingSql);
        $stmt->bindParam(':user_id', $this->userId);
        $stmt->bindParam(':record_type', $recordType);
        $stmt->bindParam(':record_id', $recordId);
        $stmt->execute();
        $existing = $stmt->fetch(PDO::FETCH_ASSOC);
        if ($existing) {
            // 更新阅读次数
            $updateSql = "UPDATE user_read_records 
                          SET read_count = read_count + 1, 
                              read_time = :read_time 
                          WHERE id = :id";
            $stmt = $this->db->prepare($updateSql);
            $stmt->bindParam(':read_time', date('Y-m-d H:i:s'));
            $stmt->bindParam(':id', $existing['id']);
            return $stmt->execute();
        } else {
            // 新增阅读记录
            $insertSql = "INSERT INTO user_read_records 
                          (user_id, record_type, record_id, read_time) 
                          VALUES (:user_id, :record_type, :record_id, :read_time)";
            $stmt = $this->db->prepare($insertSql);
            $stmt->bindParam(':user_id', $this->userId);
            $stmt->bindParam(':record_type', $recordType);
            $stmt->bindParam(':record_id', $recordId);
            $stmt->bindParam(':read_time', date('Y-m-d H:i:s'));
            return $stmt->execute();
        }
    }
    /**
     * 检查弹窗是否应该显示给当前用户
     */
    public function shouldShowPopup($popup) {
        if (!$this->userId) return true;
        switch ($popup['show_type']) {
            case 1: // 一次性
                $sql = "SELECT COUNT(*) FROM user_read_records 
                        WHERE user_id = :user_id 
                        AND record_type = 2 
                        AND record_id = :record_id";
                $stmt = $this->db->prepare($sql);
                $stmt->bindParam(':user_id', $this->userId);
                $stmt->bindParam(':record_id', $popup['id']);
                $stmt->execute();
                return $stmt->fetchColumn() == 0;
            case 2: // 每次访问
                return true;
            case 3: // 每天一次
                $today = date('Y-m-d');
                $sql = "SELECT COUNT(*) FROM user_read_records 
                        WHERE user_id = :user_id 
                        AND record_type = 2 
                        AND record_id = :record_id 
                        AND DATE(read_time) = :today";
                $stmt = $this->db->prepare($sql);
                $stmt->bindParam(':user_id', $this->userId);
                $stmt->bindParam(':record_id', $popup['id']);
                $stmt->bindParam(':today', $today);
                $stmt->execute();
                return $stmt->fetchColumn() == 0;
            default:
                return true;
        }
    }
}

前端JavaScript实现

// popup-manager.js
class PopupManager {
    constructor(options = {}) {
        this.options = {
            ajaxUrl: '/api/popup',
            markReadUrl: '/api/mark-read',
            ...options
        };
        this.init();
    }
    init() {
        this.loadAnnouncements();
        this.loadPopups();
    }
    /**
     * 加载公告
     */
    async loadAnnouncements() {
        try {
            const response = await fetch(`${this.options.ajaxUrl}?type=announcement`);
            const data = await response.json();
            if (data.success && data.data.length > 0) {
                this.renderAnnouncements(data.data);
            }
        } catch (error) {
            console.error('Load announcements failed:', error);
        }
    }
    /**
     * 加载弹窗
     */
    async loadPopups() {
        try {
            const currentPage = window.location.pathname;
            const response = await fetch(`${this.options.ajaxUrl}?type=popup&page=${encodeURIComponent(currentPage)}`);
            const data = await response.json();
            if (data.success && data.data.length > 0) {
                data.data.forEach(popup => {
                    if (this.shouldShowPopup(popup)) {
                        this.renderPopup(popup);
                    }
                });
            }
        } catch (error) {
            console.error('Load popups failed:', error);
        }
    }
    /**
     * 渲染公告列表
     */
    renderAnnouncements(announcements) {
        const container = document.getElementById('announcement-container');
        if (!container) return;
        let html = '';
        announcements.forEach(ann => {
            const isUrgent = ann.type === 3;
            html += `
                <div class="announcement-item ${isUrgent ? 'urgent' : ''}" data-id="${ann.id}">
                    <div class="announcement-header">
                        <span class="announcement-type">${this.getTypeLabel(ann.type)}</span>
                        <h4 class="announcement-title">${ann.title}</h4>
                        <span class="announcement-date">${ann.created_at}</span>
                    </div>
                    <div class="announcement-content">
                        ${ann.content}
                    </div>
                    <button class="announcement-read-btn" onclick="popupManager.markAsRead('announcement', ${ann.id})">
                        我知道了
                    </button>
                </div>
            `;
        });
        container.innerHTML = html;
    }
    /**
     * 渲染弹窗
     */
    renderPopup(popup) {
        const modal = document.createElement('div');
        modal.className = 'popup-modal';
        modal.dataset.id = popup.id;
        modal.innerHTML = `
            <div class="popup-overlay"></div>
            <div class="popup-content" style="width: ${popup.width || 400}px; max-height: ${popup.height || 300}px;">
                <button class="popup-close" onclick="popupManager.closePopup(this)">&times;</button>
                ${popup.image_url ? `<img src="${popup.image_url}" alt="${popup.title}" class="popup-image">` : ''}
                <h3 class="popup-title">${popup.title}</h3>
                <div class="popup-body">
                    ${popup.content}
                </div>
                ${popup.link_url ? `<a href="${popup.link_url}" class="popup-link" target="_blank">了解更多</a>` : ''}
            </div>
        `;
        document.body.appendChild(modal);
        // 弹窗动画
        setTimeout(() => {
            modal.classList.add('active');
        }, 100);
    }
    /**
     * 关闭弹窗
     */
    closePopup(button) {
        const modal = button.closest('.popup-modal');
        const popupId = modal.dataset.id;
        modal.classList.remove('active');
        setTimeout(() => {
            modal.remove();
        }, 300);
        // 记录关闭
        this.markAsRead('popup', popupId);
    }
    /**
     * 标记已读
     */
    async markAsRead(type, id) {
        try {
            const response = await fetch(this.options.markReadUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    type: type,
                    id: id
                })
            });
            const data = await response.json();
            if (data.success) {
                // 如果是公告,隐藏已读公告
                if (type === 'announcement') {
                    const element = document.querySelector(`.announcement-item[data-id="${id}"]`);
                    if (element) {
                        element.style.display = 'none';
                    }
                }
            }
        } catch (error) {
            console.error('Mark as read failed:', error);
        }
    }
    /**
     * 获取类型标签
     */
    getTypeLabel(type) {
        const labels = {
            1: '普通',
            2: '重要',
            3: '紧急'
        };
        return labels[type] || '普通';
    }
    /**
     * 检查是否应该显示弹窗
     */
    shouldShowPopup(popup) {
        // 检查Cookie中的关闭记录
        if (popup.show_type === 1) { // 一次性
            const closedPopups = this.getCookie('closed_popups');
            return !closedPopups || !closedPopups.includes(popup.id.toString());
        }
        if (popup.show_type === 3) { // 每天一次
            const todayClosed = this.getCookie('today_closed_popups');
            if (todayClosed) {
                const today = new Date().toDateString();
                const closedData = JSON.parse(todayClosed);
                if (closedData.date === today && closedData.ids.includes(popup.id)) {
                    return false;
                }
            }
        }
        return true;
    }
    /**
     * Cookie辅助方法
     */
    getCookie(name) {
        const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
        return match ? decodeURIComponent(match[2]) : null;
    }
    setCookie(name, value, days = 7) {
        const expires = new Date(Date.now() + days * 864e5).toUTCString();
        document.cookie = name + '=' + encodeURIComponent(value) + '; expires=' + expires + '; path=/';
    }
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
    window.popupManager = new PopupManager({
        ajaxUrl: '/api/popup.php',
        markReadUrl: '/api/mark-read.php'
    });
});

CSS样式

/* popup-styles.css */
/* 公告样式 */
.announcement-container {
    max-width: 800px;
    margin: 20px auto;
    padding: 0 15px;
}
.announcement-item {
    background: #fff;
    border: 1px solid #e0e0e0;
    border-radius: 8px;
    padding: 20px;
    margin-bottom: 15px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    transition: all 0.3s ease;
}
.announcement-item.urgent {
    border-left: 4px solid #ff4444;
    background: #fff5f5;
}
.announcement-header {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
}
.announcement-type {
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 12px;
    font-weight: bold;
}
.announcement-item .announcement-type {
    background: #007bff;
    color: white;
}
.announcement-item.urgent .announcement-type {
    background: #ff4444;
}
.announcement-title {
    flex: 1;
    margin: 0 15px;
    font-size: 16px;
}
.announcement-date {
    font-size: 12px;
    color: #888;
}
.announcement-content {
    margin: 15px 0;
    line-height: 1.6;
    color: #333;
}
.announcement-read-btn {
    background: #007bff;
    color: white;
    border: none;
    padding: 8px 20px;
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.3s;
}
.announcement-read-btn:hover {
    background: #0056b3;
}
/* 弹窗样式 */
.popup-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    z-index: 9999;
    transition: opacity 0.3s ease;
}
.popup-modal.active {
    display: flex;
    align-items: center;
    justify-content: center;
}
.popup-overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.5);
}
.popup-content {
    position: relative;
    background: white;
    border-radius: 12px;
    padding: 30px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
    max-width: 90%;
    overflow-y: auto;
    animation: popupFadeIn 0.3s ease;
}
@keyframes popupFadeIn {
    from {
        opacity: 0;
        transform: scale(0.95);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}
.popup-close {
    position: absolute;
    top: 10px;
    right: 15px;
    background: none;
    border: none;
    font-size: 28px;
    cursor: pointer;
    color: #999;
    transition: color 0.3s;
}
.popup-close:hover {
    color: #333;
}
.popup-image {
    max-width: 100%;
    height: auto;
    margin-bottom: 15px;
    border-radius: 8px;
}
.popup-title {
    margin: 0 0 15px 0;
    font-size: 20px;
    color: #333;
}
.popup-body {
    margin-bottom: 20px;
    line-height: 1.6;
    color: #666;
}
.popup-link {
    display: inline-block;
    background: #007bff;
    color: white;
    padding: 10px 25px;
    border-radius: 6px;
    text-decoration: none;
    transition: background 0.3s;
}
.popup-link:hover {
    background: #0056b3;
}
/* 响应式设计 */
@media (max-width: 768px) {
    .popup-content {
        width: 95% !important;
        margin: 10px;
    }
    .announcement-header {
        flex-direction: column;
        align-items: flex-start;
    }
    .announcement-title {
        margin: 10px 0;
    }
}

API接口示例

<?php
// api/popup.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: Content-Type');
require_once '../config/database.php';
require_once '../classes/PopupManager.php';
$db = new PDO("mysql:host=localhost;dbname=your_database", "username", "password");
$userId = $_SESSION['user_id'] ?? null;
$popupManager = new PopupManager($db, $userId);
$type = $_GET['type'] ?? '';
$page = $_GET['page'] ?? '';
switch ($type) {
    case 'announcement':
        $announcements = $popupManager->getActiveAnnouncements();
        echo json_encode([
            'success' => true,
            'data' => $announcements
        ]);
        break;
    case 'popup':
        $popups = $popupManager->getActivePopups($page);
        $filteredPopups = [];
        foreach ($popups as $popup) {
            if ($popupManager->shouldShowPopup($popup)) {
                $filteredPopups[] = $popup;
            }
        }
        echo json_encode([
            'success' => true,
            'data' => $filteredPopups
        ]);
        break;
    default:
        echo json_encode([
            'success' => false,
            'message' => 'Invalid type'
        ]);
}
?>

管理后台示例

<?php
// admin/announcements.php
// 创建公告
if ($_POST['action'] == 'create') {
    $title = $_POST['title'];
    $content = $_POST['content'];
    $type = $_POST['type'];
    $isTop = $_POST['is_top'] ?? 0;
    $startTime = $_POST['start_time'] ?? null;
    $endTime = $_POST['end_time'] ?? null;
    $sql = "INSERT INTO announcements (title, content, type, is_top, start_time, end_time, status) 
            VALUES (:title, :content, :type, :is_top, :start_time, :end_time, 1)";
    $stmt = $db->prepare($sql);
    $stmt->execute([
        ':title' => $title,
        ':content' => $content,
        ':type' => $type,
        ':is_top' => $isTop,
        ':start_time' => $startTime,
        ':end_time' => $endTime
    ]);
}
// 更新公告
if ($_POST['action'] == 'update') {
    $id = $_POST['id'];
    $title = $_POST['title'];
    $content = $_POST['content'];
    $type = $_POST['type'];
    $isTop = $_POST['is_top'] ?? 0;
    $status = $_POST['status'] ?? 1;
    $sql = "UPDATE announcements SET 
            title = :title, 
            content = :content, 
            type = :type, 
            is_top = :is_top, 
            status = :status 
            WHERE id = :id";
    $stmt = $db->prepare($sql);
    $stmt->execute([
        ':title' => $title,
        ':content' => $content,
        ':type' => $type,
        ':is_top' => $isTop,
        ':status' => $status,
        ':id' => $id
    ]);
}
// 删除公告
if ($_POST['action'] == 'delete') {
    $id = $_POST['id'];
    $sql = "DELETE FROM announcements WHERE id = :id";
    $stmt = $db->prepare($sql);
    $stmt->execute([':id' => $id]);
}

使用说明

  1. 安装:将数据库表结构和PHP文件部署到服务器
  2. 配置:修改数据库连接配置
  3. 前端集成:在页面中引入CSS和JS文件
  4. 管理:使用管理后台创建和管理公告与弹窗

这个系统支持:

  • 公告的CRUD操作
  • 弹窗的多种显示方式
  • 用户阅读记录追踪
  • 定时发布和下线
  • 多种弹窗类型和位置
  • 响应式设计

可根据实际需求进行扩展和定制。

抱歉,评论功能暂时关闭!