本文目录导读:

我来为您提供一个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)">×</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]);
}
使用说明
- 安装:将数据库表结构和PHP文件部署到服务器
- 配置:修改数据库连接配置
- 前端集成:在页面中引入CSS和JS文件
- 管理:使用管理后台创建和管理公告与弹窗
这个系统支持:
- 公告的CRUD操作
- 弹窗的多种显示方式
- 用户阅读记录追踪
- 定时发布和下线
- 多种弹窗类型和位置
- 响应式设计
可根据实际需求进行扩展和定制。