关于开发社区网站小组发帖功能和多级回复的代码
init.php
<?php // init.php - 数据库初始化脚本 include "config.php"; if (!$pdo) { die("数据库连接失败"); } // 设置错误模式为异常,便于捕获 $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // ------------------------------ // 1. 用户表 // ------------------------------ $sqlUser = "CREATE TABLE IF NOT EXISTS user ( id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, account VARCHAR(100) NOT NULL UNIQUE, introduction TEXT, password VARCHAR(100) NOT NULL, headImgUrl VARCHAR(250), userName VARCHAR(20) UNIQUE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB"; // ------------------------------ // 2. 作品表(依赖 user) // ------------------------------ $sqlArtwork = "CREATE TABLE IF NOT EXISTS artworks ( id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, title VARCHAR(80) NOT NULL, art_type ENUM('music','voice','novel','comic','game','chahua') NOT NULL, contentUrl VARCHAR(128) NOT NULL, progress INT(11) NOT NULL DEFAULT 0, description TEXT NOT NULL, privacy ENUM('public', 'private') NOT NULL DEFAULT 'private', creator_uid INT(11) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (creator_uid) REFERENCES user(id) ON DELETE CASCADE, UNIQUE KEY unique_user_artwork (creator_uid, title) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB"; // ------------------------------ // 3. 小组表 // ------------------------------ $sqlGroups = "CREATE TABLE IF NOT EXISTS groups ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description TEXT, creator_uid INT(11) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB"; // ------------------------------ // 4. 小组成员表(依赖 groups 和 user) // ------------------------------ $sqlGroupMembers = "CREATE TABLE IF NOT EXISTS group_members ( id INT(11) PRIMARY KEY AUTO_INCREMENT, group_id INT(11) NOT NULL, member_id INT(11) NOT NULL, role ENUM('member', 'creator', 'admin') DEFAULT 'member', joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE, FOREIGN KEY (member_id) REFERENCES user(id) ON DELETE CASCADE, UNIQUE KEY unique_member_per_group (group_id, member_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB"; // ------------------------------ // 5. 帖子表(依赖 groups) // ------------------------------ $sqlPosts = "CREATE TABLE IF NOT EXISTS posts ( id INT PRIMARY KEY AUTO_INCREMENT, group_id INT NOT NULL, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB"; // ------------------------------ // 6. 评论表(依赖 posts,自关联) // ------------------------------ $sqlComments = "CREATE TABLE IF NOT EXISTS comments ( id INT PRIMARY KEY AUTO_INCREMENT, post_id INT NOT NULL, user_id INT NOT NULL, parent_id INT NULL, content TEXT NOT NULL, depth INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE, FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB"; // ------------------------------ // 执行建表(按依赖顺序) // ------------------------------ try { $pdo->exec($sqlUser); echo "✅ 用户表创建成功<br>"; $pdo->exec($sqlArtwork); echo "✅ 作品表创建成功<br>"; $pdo->exec($sqlGroups); echo "✅ 小组表创建成功<br>"; $pdo->exec($sqlGroupMembers); echo "✅ 小组成员表创建成功<br>"; $pdo->exec($sqlPosts); echo "✅ 帖子表创建成功<br>"; $pdo->exec($sqlComments); echo "✅ 评论表创建成功<br>"; // ------------------------------ // 7. 创建索引(MySQL 5.x 兼容写法:不检测存在,直接创建,重复则捕获异常) // ------------------------------ $indexes = [ "CREATE INDEX idx_comments_post_id ON comments(post_id)", "CREATE INDEX idx_comments_parent_id ON comments(parent_id)", "CREATE INDEX idx_comments_created_at ON comments(created_at)" ]; foreach ($indexes as $indexSql) { try { $pdo->exec($indexSql); } catch (PDOException $e) { // 索引可能已存在,忽略错误(或输出提示) // 如果你想严格检查,可以在此处记录日志,但不影响后续运行 } } echo "✅ 评论表索引创建成功<br>"; echo "<br>🎉 数据库初始化完成!所有表和外键已建立。"; } catch (PDOException $e) { echo "数据库操作失败: " . $e->getMessage(); // 可打印详细错误码以便调试(生产环境建议关闭) // echo "<br>错误码: " . $e->getCode(); } ?>
groupViewer.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $_GET["groupName"];?></title>
<script src="/js/thirdparty/artDialog-7.0.0/lib/jquery-1.10.2.js"></script>
<script src="/js/thirdparty/artDialog-7.0.0/dist/dialog.js"></script>
</head>
<body>
<?php echo "<h2>" . $_GET["groupName"] . "</h2>";?>
<button onclick="joinGroup()">加入这个小组</button>
<button onclick="publishNew()">发表新主题</button>
<h2>小组最新讨论</h2>
<?php echo "<p>创建于" . $_GET["created_at"] . "</p>"; ?>
<ol>
<?php
include "../php/config.php";
try
{
$stmt = $pdo->prepare("SELECT * FROM posts WHERE group_id = :group_id");
$stmt->bindParam(":group_id", $_GET["id"]);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $row)
{
// 1. 对参数值进行 URL 编码
$safeTitle = urlencode($row["title"]);
$safeUserId = urlencode($row["user_id"]);
$safeCreatedAt = urlencode($row["created_at"]);
$safePostId = urlencode($row["id"]);
$displayTitle = htmlspecialchars($row["title"], ENT_QUOTES, 'UTF-8');
echo "<li><a href=\"tieziViewer.php?title={$safeTitle}&user_id={$safeUserId}&created_at={$safeCreatedAt}&post_id={$safePostId}&group_id={$_GET['id']}\">{$displayTitle}</a></li>";
}
}catch (PDOException $e)
{
echo "Error: " . $e->getMessage();
}
?>
</ol>
<script>
function joinGroup()
{
var data = new FormData()
data.append("action", "joinGroup")
const groupID = "<?php echo htmlspecialchars($_GET['id'], ENT_QUOTES, 'UTF-8'); ?>";
data.append("group_id", groupID)
const request = new XMLHttpRequest()
request.open("POST","../php/group.php")
request.onload = function()
{
if (request.status == 200)
{
var json = JSON.parse(request.responseText)
console.log(json)
if (json["status"] == "success")
{
var tip = dialog({title:"提示",content:json["message"]})
tip.show()
}
else
{
var tip = dialog({title:"提示",content:json["message"]})
tip.show()
}
}
}
request.send(data)
}
function publishNew()
{
var d = dialog({
title: "发表新主题",
content: `
<input id=title type="" placeholder="标题"><br/>
<textarea id=content placeholder="内容"></textarea>
`,
ok: function()
{
var title = document.getElementById("title").value
var content = document.getElementById("content").value
var data = new FormData()
data.append("action", "addNewTieZi")
data.append("group_id", "<?php echo htmlspecialchars($_GET['id'], ENT_QUOTES, 'UTF-8'); ?>")
data.append("title", title)
data.append("content", content)
const request = new XMLHttpRequest()
request.open("POST","../php/group.php")
request.onload = function()
{
if (request.status == 200)
{
console.log(request.responseText)
var json = JSON.parse(request.responseText)
console.log(json)
if (json["status"] == "success")
{
var tip = dialog({
title: "提示",
content: json["message"],
onclose: function()
{
location.reload()
}
})
tip.show()
}
else{
var tip = dialog({title:"提示",content: json["message"]})
tip.show()
}
}
}
request.send(data)
}
})
d.show();
}
</script>
</body>
</html>
grop.php
<?php session_start(); include "config.php"; /* 2. 嵌套回复的核心原理 邻接表模型:每一行只记录自己的直接父级(parent_id)。 查询时,从根(parent_id IS NULL)开始,递归获取所有子节点。 深度字段(depth):可选但推荐。在插入回复时,通过 parent_id 查出父回复的 depth + 1 写入。 这样前端可以直接用 depth * 24px 等做缩进,无需递归计算。 排序:通常按 created_at 正序,也可以实现“按楼层”或“点赞”排序。 4. 性能考虑与扩展 深嵌套查询:递归 CTE 在深度超过 10 级时效率尚可,但若帖子有上万条回复,建议使用 闭包表(Closure Table)或 路径枚举,不过邻接表 + depth 已满足绝大多数社区需求。 回复计数:可在 posts 表增加 comment_count 冗余字段,通过触发器或业务代码更新,避免每次 COUNT(*)。 小组内的帖子数:同样可冗余到 groups 表。 5. 与前端“树形DOM+缩进折叠”的配合 后端返回树形 JSON(如上面的 $tree),前端递归渲染即可。 缩进:每个回复项使用 margin-left: ${depth * 24}px,或者用 CSS 类 .depth-0、.depth-1 控制。 折叠状态:前端维护一个 collapsedCommentIds 集合,渲染时跳过渲染子节点或使用 display: none。 6. 小结 表 作用 关键字段 groups 小组容器 id, name posts 帖子 id, group_id, title, content comments 支持无限嵌套的回复 id, post_id, parent_id, depth 这套设计简洁、扩展性好,完全能满足你“小组内多帖子,每个帖子有任意多级回复”的需求,且不会引入传统论坛的重度依赖。 */ if ($_SERVER["REQUEST_METHOD"] == "POST") { $action = $_POST['action']; if ($action == "newGroup") { newGroup($_POST["groupName"], $_SESSION["uid"]); } else if ($action == "joinGroup") { joinGroup($_POST["group_id"], $_SESSION["uid"]); } else if ($action == "addNewTieZi") { addNewTieZi($_POST["group_id"], $_SESSION["uid"], $_POST["title"], $_POST["content"]); } // 一级回复 else if ($action == "replyLouZhu") { replyLouZhu($_POST["post_id"], $_SESSION["uid"], $_POST["content"]); } else if ($action == "reply") { reply($_POST["post_id"], $_SESSION["uid"], $_POST["parent_id"], $_POST["content"],$_POST["depth"]); } // 帖子顶层修改 else if ($action == "modifyTieZiRoot") { modifyTieZiRoot($_POST["post_id"], $_SESSION["uid"], $_POST["title"], $_POST["content"]); } else if ($action == "deleteTieZi") { deleteTieZi($_POST["post_id"], $_POST["group_id"], $_SESSION["uid"], ); } else if ($action == "editLayered") { modifyLayered($_POST["comment_id"], $_SESSION["uid"], $_POST["content"]); } else if ($action == "deleteLayered") { deleteLayered($_POST["comment_id"], $_SESSION["uid"]); } } else if ($_SERVER["REQUEST_METHOD"] == "GET") { $action = $_GET['action']; if ($action == "queryAllGroupName") { queryAllGroupName(); } } function modifyTieZiRoot($postId, $actionUser, $title, $content) { global $pdo; $data = array(); try { $stmt = $pdo->prepare("UPDATE posts SET title = :title, content = :content WHERE id = :id AND user_id = :user_id"); $stmt->bindParam(":title", $title); $stmt->bindParam(":content", $content); $stmt->bindParam(":id", $postId); $stmt->bindParam(":user_id", $actionUser); if ($stmt->execute()) { $data["status"] = "success"; $data["message"] = "修改成功"; echo json_encode($data); } else{ $data["status"] = "error"; $data["message"] = "修改失败:数据库执行错误"; echo json_encode($data); } }catch(PDOException $e) { $data["status"] = "error"; $data["message"] = "修改失败:" . $e->getMessage(); echo json_encode($data); } } // 多级回复编辑 function modifyLayered($commentId, $actionUser, $content) { global $pdo; $data = array(); try { $stmt = $pdo->prepare("UPDATE comments SET content = :content WHERE id = :id AND user_id = :user_id"); $stmt->bindParam(":content", $content); $stmt->bindParam(":id", $commentId); $stmt->bindParam(":user_id", $actionUser); if($stmt->execute()) { $data["status"] = "success"; $data["message"] = "修改成功"; echo json_encode($data); } else{ $data["status"] = "error"; $data["message"] = "修改失败:数据库执行错误"; echo json_encode($data); } } catch(PDOException $e) { $data["status"] = "error"; $data["message"] = "修改失败:" . $e->getMessage(); echo json_encode($data); } } function deleteLayered($commentId, $actionUser) { global $pdo; $data = array(); try{ // 先检查权限:该评论是否属于当前用户 $stmt = $pdo->prepare("SELECT user_id FROM comments WHERE id = ?"); $stmt->execute([$commentId]); $comment = $stmt->fetch(); if (!$comment || $comment['user_id'] != $actionUser) { $data["status"] = "error"; $data["message"] = "无权删除此回复"; echo json_encode($data); return; } // 如果设置了外键 ON DELETE CASCADE,只需删除本评论,子评论会自动删除 $stmt = $pdo->prepare("DELETE FROM comments WHERE id = ?"); $stmt->execute([$commentId]); $data["status"] = "success"; $data["message"] = "删除成功"; echo json_encode($data); } catch(PDOException $e) { $data["status"] = "error"; $data["message"] = "修改失败:" . $e->getMessage(); echo json_encode($data); } } // 查询数据表 返回树 function queryGroupJson($groupName) { /*global $pdo; // 查出某个帖子下所有评论(扁平数组) $rows = $pdo->query("SELECT * FROM comments WHERE post_id = ? ORDER BY created_at", [$postId]); // 构建树形结构 $comments = []; foreach ($rows as $row) { $comments[$row['id']] = $row; } $tree = []; foreach ($comments as &$item) { if ($item['parent_id'] === null) { $tree[] = &$item; } else { $comments[$item['parent_id']]['children'][] = &$item; } } // 最后 $tree 就是多级嵌套的回复树 echo json_encode($tree);*/ } function queryGroup($groupId) { global $pdo; //$pdo->prepare("SELECT FROM comments WHERE id = ?") } function queryAllGroupName() { global $pdo; $sql = "SELECT name,id,created_at from groups"; $data = array(); try { $stmt = $pdo->prepare($sql); $stmt->execute(); // 【关键修改】使用 execute() 而不是 exec() $result = $stmt->fetchAll(PDO::FETCH_ASSOC); $data["status"] = "success"; $data["content"] = $result; // 返回 JSON echo json_encode($data); } catch (PDOException $e) { // 出错时返回错误 JSON,防止前端解析空字符串 $data["status"] = "error"; $data["message"] = "查询失败:数据库执行错误" . $e->getMessage(); echo json_encode($data); } } function newGroup($groupName, $actionUser) { global $pdo; $data = array(); try { $stmt = $pdo->prepare("INSERT INTO groups (name, creator_uid) VALUES (:name, :creator_uid) "); $stmt->bindParam(":name",$groupName); $stmt->bindParam(":creator_uid",$actionUser); if($stmt->execute()) { $data["status"] = "success"; $data["message"] = "创建成功"; } else{ $data["status"] = "error"; $data["message"] = "创建失败:数据库执行错误"; } }catch(PDOException $e) { $data["status"] = "error"; $data["message"] = "创建失败:" . $e->getMessage(); } echo json_encode($data); } function joinGroup($groupId, $actionUser) { global $pdo; $data = array(); try { $stmt = $pdo->prepare("INSERT INTO group_members (group_id, member_id, role) VALUES (:group_id, :member_id, :role)"); $stmt->bindParam(":group_id", $groupId); $stmt->bindParam(":member_id", $actionUser); $stmt->bindValue(":role", "member"); if ($stmt->execute()) { $data["status"] = "success"; $data["message"] = "加入小组成功"; echo json_encode($data); } else { $data["status"] = "error"; $data["message"] = "加入小组失败:数据库执行错误"; echo json_encode($data); } } catch(PDOException $e) { $data["status"] = "error"; $data["message"] = "加入小组失败:数据库执行错误" . $e->getMessage(); echo json_encode($data); } } function quitGroup($groupName, $actionUser) { $data = array(); $data["tip"] = "已退出"; echo json_encode($data); } function addNewTieZi($groupId, $actionUser, $title, $content) { global $pdo; $data = array(); $stmt = $pdo->prepare("INSERT INTO posts (group_id, user_id, title, content) VALUES (:group_id, :user_id, :title, :content)"); $stmt->bindParam(":group_id", $groupId); $stmt->bindParam(":user_id", $actionUser); $stmt->bindParam(":title", $title); $stmt->bindParam(":content", $content); try{ if ($stmt->execute()) { $data["status"] = "success"; $data["message"] = "发布成功"; echo json_encode($data); } else { $data["status"] = "error"; $data["message"] = "发布失败:数据库执行错误"; echo json_encode($data); } } catch (PDOException $e) { $data["status"] = "error"; $errCode = $e->getCode(); $errMsg = $e->getMessage(); //$data["code"] = $e->getCode(); // 23000 通常代表完整性约束违规(如外键失败、非空约束等) if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) { $data["message"] = "您尚未登录或会话已过期,请重新登录后再试!"; } else { // 其他错误记录日志,但不一定把详细 SQL 错误抛给前端(安全考虑) error_log("发生了一个错误\n", 3, __DIR__ . "/my_errors.log"); $data["message"] = "发布失败:系统内部错误,请联系管理员。"; } echo json_encode($data); } /*if ($actionUser is not in $groupName) { $data["tip"] = "你不在这个小组里,无法发布帖子"; } else { $data=["tip"] = "发布成功"; }*/ } // 一级回复 function replyLouZhu($postId, $actionUser, $content) { global $pdo; $data = array(); try{ $stmt = $pdo->prepare("INSERT INTO comments (post_id, user_id, content) VALUES (:post_id, :user_id, :content)"); $stmt->bindParam(":post_id", $postId); $stmt->bindParam(":user_id", $actionUser); $stmt->bindParam(":content", $content); if ($stmt->execute()) { $data["status"] = "success"; $data["message"] = "回复成功"; echo json_encode($data); } else { $data["status"] = "error"; $data["message"] = "回复失败:数据库执行错误"; echo json_encode($data); } }catch (PDOException $e) { $data["status"] = "error"; $errCode = $e->getCode(); $errMsg = $e->getMessage(); // 23000 通常代表完整性约束违规(如外键失败、非空约束等) if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) { $data["message"] = "您尚未登录或会话已过期,请重新登录后再试!"; echo json_encode($data); } else { // 其他错误记录日志,但不一定把详细 SQL 错误抛给前端(安全考虑) error_log("发生了一个错误\n", 3, __DIR__ . "/my_errors.log"); $data["message"] = "发布失败:系统内部错误,请联系管理员。"; echo json_encode($data); } } } // 多级回复 function reply($postId, $actionUser, $parent_id, $content) { global $pdo; try{ $stmt = $pdo->prepare("SELECT depth FROM commments WHERE id = :parent_id"); $stmt->bindParam(":parent_id", $parent_id); $parent = $stmt->fetch(PDO::FETCH_ASSOC); $newDepth = $parent ? $parent['depth'] + 1 : 0; // 如果父评论不存在,深度设为0(防错) $stmt = $pdo->prepare("INSERT INTO comments (post_id, user_id, parent_id, content, depth) VALUES (:post_id, :user_id, :parent_id, :content, :depth)"); $stmt->bindParam(":post_id", $postId); $stmt->bindParam(":user_id", $actionUser); $stmt->bindParam(":parent_id", $parent_id); $stmt->bindParam(":content", $content); $stmt->bindParam(":depth", $newDepth); if ($stmt->execute()) { $data["status"] = "success"; $data["message"] = "回复成功"; echo json_encode($data); } else { $data["status"] = "error"; $data["message"] = "回复失败:数据库执行错误"; echo json_encode($data); } }catch (PDOException $e) { $data["status"] = "error"; $errCode = $e->getCode(); $errMsg = $e->getMessage(); // 23000 通常代表完整性约束违规(如外键失败、非空约束等) if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) { $data["message"] = "您尚未登录或会话已过期,请重新登录后再试!"; } else { // 其他错误记录日志,但不一定把详细 SQL 错误抛给前端(安全考虑) error_log("发生了一个错误\n", 3, __DIR__ . "/my_errors.log"); $data["message"] = "发布失败:系统内部错误,请联系管理员。"; } } } function deleteTieZi($postId, $groupId, $actionUser) { global $pdo; $data = array(); try { // 只需删除帖子,comments 的外键 ON DELETE CASCADE 会自动删除所有评论 $stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id AND group_id = :group_id AND user_id = :user_id"); $stmt->bindParam(":id", $postId); $stmt->bindParam(":group_id", $groupId); $stmt->bindParam(":user_id", $actionUser); if ($stmt->execute()) { $data["status"] = "success"; $data["message"] = "删除成功"; } else { $data["status"] = "error"; $data["message"] = "删除失败:数据库执行错误"; } echo json_encode($data); } catch (PDOException $e) { $data["status"] = "error"; $errCode = $e->getCode(); $errMsg = $e->getMessage(); $data["code"] = $errCode; $data["debug"] = $errMsg; if ($errCode == '23000' || strpos($errMsg, 'user_id cannot be null') !== false) { $data["message"] = "您尚未登录或会话已过期"; } else { $data["message"] = "删除失败:系统内部错误,请联系管理员。"; error_log("删除帖子错误: " . $errMsg . "\n", 3, __DIR__ . "/my_errors.log"); } echo json_encode($data); } } ?>
tieziViewer.php
<?php session_start(); include "../php/config.php"; $data = array(); try{ $stmt = $pdo->prepare("SELECT userName, headImgUrl FROM user WHERE id = :id"); $stmt->bindParam(":id", $_GET["user_id"]); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); $user = $result["userName"]; $creator_head = "<img alt=无法显示头像 width=100px src=" . $result["headImgUrl"] .">"; if ($user == null) { $user = "匿名用户"; } }catch(PDOException $e) { echo $e->getMessage(); } try{ $stmt = $pdo->prepare("SELECT title, content FROM posts WHERE id = :id"); $stmt->bindParam(":id", $_GET["post_id"]); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); $tieZiContent = $result["content"]; $tieZiTitle = $result["title"]; }catch(PDOException $e) { die('数据库错误:' . $e->getMessage()); } // 获取该帖子的所有评论(扁平数据) try{ $stmt = $pdo->prepare("SELECT * FROM comments WHERE post_id = :post_id ORDER BY created_at ASC"); $stmt->bindParam(":post_id", $_GET["post_id"]); $stmt->execute(); $commentsFlat = $stmt->fetchAll(PDO::FETCH_ASSOC); } catch(PDOException $e) { die('数据库错误:' . $e->getMessage()); } // 构建评论树(邻接表 → 树形结构) function buildTree($comments, $parentId = null) { $tree = []; foreach ($comments as $comment) { if ($comment['parent_id'] == $parentId) { $children = buildTree($comments, $comment['id']); if ($children) { $comment['children'] = $children; } else { $comment['children'] = []; } $tree[] = $comment; } } return $tree; } $commentTree = buildTree($commentsFlat); // 递归生成评论HTML(带缩进和折叠按钮) function renderComment($comment, $depth = 0) { global $pdo; $indentStyle = $depth > 0 ? 'style="margin-left: ' . ($depth * 28) . 'px;"' : ''; $collapseId = 'comment-' . $comment['id']; $childrenHtml = ''; if (!empty($comment['children'])) { $childrenHtml = '<div class="comment-children" id="children-' . $comment['id'] . '">'; foreach ($comment['children'] as $child) { $childrenHtml .= renderComment($child, $depth + 1); } $childrenHtml .= '</div>'; } $hasChildren = !empty($comment['children']); // $author = htmlspecialchars($comment['user_id'] ?? '匿名用户', ENT_QUOTES); $stmt = $pdo->prepare("SELECT userName, headImgUrl FROM user WHERE id = :id"); $stmt->bindParam(':id', $comment['user_id']); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); $author = $result["userName"] ?? "匿名用户"; $authorHead = <<<AUTHORHEAD <img alt=无法显示头像 width=100px src="{$result['headImgUrl']}"> AUTHORHEAD; $content = nl2br(htmlspecialchars($comment['content'], ENT_QUOTES)); $time = htmlspecialchars($comment['created_at'], ENT_QUOTES); // ✅ 根据登录状态和权限生成编辑/删除按钮 $actionButtons = ''; if (isset($_SESSION["uid"]) && $_SESSION["uid"] == $comment['user_id']) { // 将评论的 id 和 depth 作为参数传递给 JS 函数 $actionButtons = <<<BTN <button class="edit-btn" onclick="editLayered({$comment['id']}, '{$comment['content']}')">编辑</button> <button class="delete-btn" onclick="deleteLayered({$comment['id']}, {$comment['depth']})">删除</button> BTN; } return <<<HTML <div class="comment-item" data-comment-id="{$comment['id']}" {$indentStyle}> <div class="comment-meta"> {$authorHead} <strong>{$author}</strong> <span class="time">{$time}</span> <button class="reply-btn" data-parent-id="{$comment['id']}" data-author="{$author}">回复</button> {$hasChildren} <button class="collapse-btn" data-target="children-{$comment['id']}">折叠</button> {$actionButtons} </div> <div class="comment-content">{$content}</div> {$childrenHtml} <div class="reply-form-container" id="reply-form-{$comment['id']}" style="display:none;"> <form method="post" class="inline-reply-form"> <input type="hidden" name="action" value="reply"> <input type="hidden" name="post_id" value="{$_GET["post_id"]}"> <input type="hidden" name="parent_id" value="{$comment['id']}">> <textarea name="content" rows="2" placeholder="回复 {$author}..."></textarea> <button type="submit">提交回复</button> <button type="button" class="cancel-reply">取消</button> </form> </div> </div> HTML; } // 生成所有顶级评论的HTML $commentsHtml = ''; foreach ($commentTree as $rootComment) { $commentsHtml .= renderComment($rootComment); } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo $tieZiTitle;?></title> <script src="/js/thirdparty/artDialog-7.0.0/lib/jquery-1.10.2.js"></script> <script src="/js/thirdparty/artDialog-7.0.0/dist/dialog.js"></script> <style> body { font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 20px auto; padding: 20px; background: #f5f7fb; } .post-container { background: white; border-radius: 24px; padding: 24px; margin-bottom: 30px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .post-title { font-size: 1.8rem; font-weight: 700; margin-bottom: 12px; } .post-meta { color: #64748b; font-size: 0.85rem; margin-bottom: 20px; border-bottom: 1px solid #eef2ff; padding-bottom: 12px; } .post-content { line-height: 1.6; font-size: 1rem; } .comments-section { background: white; border-radius: 24px; padding: 24px; } .comment-item { margin-top: 16px; padding: 12px; background: #f9fafb; border-radius: 20px; border: 1px solid #eef2ff; transition: 0.1s; } .comment-meta { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; } .comment-meta strong { color: #0f172a; } .time { font-size: 0.7rem; color: #94a3b8; } .comment-content { font-size: 0.9rem; margin: 8px 0 10px 0; word-break: break-word; } button { background: #eef2ff; padding: 4px 12px; border-radius: 30px; font-size: 0.7rem; cursor: pointer; font-weight: 500; border: solid 1px; } button:hover { background: #e2e8f0; } .comment-children { margin-top: 8px; } .reply-form-container { margin-top: 12px; } .inline-reply-form { background: white; padding: 12px; border-radius: 20px; border: 1px solid #cbd5e1; } .inline-reply-form textarea { width: 100%; border-radius: 14px; border: 1px solid #cbd5e1; padding: 8px; font-family: inherit; resize: vertical; } .inline-reply-form input { margin: 8px 0; padding: 6px 10px; border-radius: 40px; border: 1px solid #cbd5e1; } .inline-reply-form button { margin-right: 8px; } .root-reply { margin-top: 28px; background: #f1f5f9; padding: 20px; border-radius: 24px; } .collapsed .comment-children { display: none; } .error-msg { background: #fee2e2; color: #991b1b; padding: 10px; border-radius: 16px; margin-bottom: 20px; } </style> </head> <body> <div class="post-container"> <div class="post-title"><?php echo htmlspecialchars($tieZiTitle, ENT_QUOTES); ?></div> <div class="post-meta"> <?php echo $creator_head ;?> 作者:<?php echo htmlspecialchars($user ?? '匿名用户', ENT_QUOTES); ?> 发表于 <?php echo htmlspecialchars($_GET['created_at'], ENT_QUOTES); ?> <?php if ($_SESSION["uid"] == $_GET["user_id"]): ?> <button onclick="editTop()">编辑</button> <button onclick="deleteTieZi()">删除</button> <?php endif;?> </div> <div class="post-content"><?php echo $tieZiContent; ?></div> </div> <div class="comments-section"> <h3>💬 树形回复(可折叠缩进)</h3> <?php if (isset($error)): ?> <div class="error-msg"><?php echo htmlspecialchars($error); ?></div> <?php endif; ?> <div id="commentsTree"> <?php echo $commentsHtml ?: '<div style="text-align:center; padding: 30px;">✨ 暂无回复,来抢沙发吧~</div>'; ?> </div> <!-- 回复楼主的表单 --> <div class="root-reply"> <form method="post" id="replyLouZhu"> <input type="hidden" name="action" value="replyLouZhu"> <input type="hidden" name="post_id" value=<?php echo $_GET["post_id"];?>> <textarea name="content" rows="3" cols="100px" placeholder="发表你的看法..." required></textarea><br> <button type="submit">发布回复</button> </form> </div> </div> <script> // 折叠/展开功能 document.querySelectorAll('.collapse-btn').forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const targetId = this.dataset.target; const targetDiv = document.getElementById(targetId); if (targetDiv) { targetDiv.classList.toggle('collapsed'); this.textContent = targetDiv.classList.contains('collapsed') ? '展开' : '折叠'; } }); }); // 回复按钮:显示对应的内嵌回复表单 document.querySelectorAll('.reply-btn').forEach(btn => { btn.addEventListener('click', function(e) { e.stopPropagation(); const parentId = this.dataset.parentId; const formContainer = document.getElementById(`reply-form-${parentId}`); if (formContainer) { // 隐藏其他打开的表单 document.querySelectorAll('.reply-form-container').forEach(container => { if (container.id !== `reply-form-${parentId}`) container.style.display = 'none'; }); formContainer.style.display = formContainer.style.display === 'none' ? 'block' : 'none'; } }); }); // 取消回复 document.querySelectorAll('.cancel-reply').forEach(btn => { btn.addEventListener('click', function() { this.closest('.reply-form-container').style.display = 'none'; }); }); function editTop() { var d = dialog({ title: "提示", content: ` <input id="editTopTitleInput" type="text" name="title" placeholder="标题" value=""><br/> <textarea id="editTopContent" name="content" placeholder="内容" cols="100px"></textarea> `, ok: function() { var data = new FormData() data.append("action", "modifyTieZiRoot") data.append("group_id", "<?php echo htmlspecialchars($_GET['id'], ENT_QUOTES, 'UTF-8'); ?>") data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>") data.append("title", document.getElementById("editTopTitleInput").value) data.append("content", document.getElementById("editTopContent").value) const request = new XMLHttpRequest(); request.open('POST', "../php/group.php"); request.onload = function() { if (request.status === 200) { var json = JSON.parse(request.responseText) if (json.status == "success") { var tip = dialog({ title: "提示", content: json.message, ok: function() { window.location.reload() } }) tip.show() } else{ var tip = dialog({ title: "提示", content: json.message, }) tip.show() } } } request.send(data); } }) d.show() } // 辅助函数防XSS function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } function editLayered(commentId, currentContent) { var d = dialog({ title: "提示", content: ` <textarea id="editLayeredReplayContent" name="content" placeholder="内容" cols="100px">${escapeHtml(currentContent)}</textarea> `, ok: function() { var data = new FormData() data.append("action", "editLayered") data.append("comment_id", commentId) // data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>") data.append("content", document.getElementById("editLayeredReplayContent").value) const request = new XMLHttpRequest(); request.open("POST", "../php/group.php") request.onload = function() { if (request.status === 200) { var json = JSON.parse(request.responseText) if (json.status === "success") { var tip = dialog({ title: "提示", content: json.message, ok: function() { window.location.reload() } }) tip.show() } else{ var tip = dialog({ title: "提示", content: json.message, ok: function() { window.location.reload() } }) tip.show() } } else { alert("操作失败") } } request.send(data) } }) d.show() } function deleteLayered(commentId, depth) { var d = dialog({ title: "提示", content: "确定删除该回复吗?其所有子回复也将被删除。", ok: function() { var data = new FormData() data.append("action", "deleteLayered") data.append("comment_id", commentId) // data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>") // data.append("parent_id", "<?php echo htmlspecialchars($comment['id'], ENT_QUOTES, 'UTF-8'); ?>") // data.append("depth", "<?php echo htmlspecialchars($comment['depth'], ENT_QUOTES, 'UTF-8'); ?>") const request = new XMLHttpRequest() request.open('POST', "../php/group.php") request.onload = function() { if (request.status === 200) { console.log(request.responseText) var json = JSON.parse(request.responseText) if (json.status === "success") { var tip = dialog({ title: "提示", content: json.message, onclose: function() { window.location.reload() } }) tip.show() } else{ var tip = dialog({ title: "提示", content: json.message, onclose: function() { window.location.reload() } }) tip.show() } } } request.send(data) } }) d.show() } function deleteTieZi() { var d = dialog({ title: "提示", content: "确定要删除吗?", ok: function() { var data = new FormData() data.append("action", "deleteTieZi") data.append("post_id", "<?php echo htmlspecialchars($_GET['post_id'], ENT_QUOTES, 'UTF-8'); ?>") data.append("user_id", "<?php echo htmlspecialchars($_GET['user_id'], ENT_QUOTES, 'UTF-8'); ?>") data.append("group_id", "<?php echo htmlspecialchars($_GET['group_id'], ENT_QUOTES, 'UTF-8'); ?>") const request = new XMLHttpRequest() request.open('POST', "../php/group.php") request.onload = function() { if (request.status === 200) { const json = JSON.parse(request.responseText) if (json.status == "success") { var tip = dialog({ title: "提示", content: "删除成功", onclose: function() { window.location.href = "groupViewer.php" } }) tip.show() } else{ var tip = dialog({ title: "提示", content: json.message }) tip.show() } } } request.send(data) } }) d.show() } const replyLouZhuForm = document.getElementById('replyLouZhu'); replyLouZhuForm.addEventListener("submit", function(e) { e.preventDefault() var formData = new FormData(this) const submitBtn = this.querySelector('button[type="submit"]') submitBtn.disabled = true submitBtn.textContent = '提交中...' const request = new XMLHttpRequest() request.open('POST', "../php/group.php") request.onload = function() { if (request.status === 200) { const json = JSON.parse(request.responseText) if (json.status == "success") { var tip = dialog({ title: "提示", content: "回复成功", onclose: function() { window.location.reload() } }) tip.show() } else { var tip = dialog({ title: "提示", content: json.message, }) tip.show() } } } request.send(formData) }) const replyForms = document.getElementsByClassName("inline-reply-form") for (var i = 0; i < replyForms.length; i++) { const replyForm = replyForms[i] replyForm.addEventListener("submit", function(e) { e.preventDefault() var formData = new FormData(this) const submitBtn = this.querySelector('button[type="submit"]') submitBtn.disabled = true submitBtn.textContent = '提交中...' const request = new XMLHttpRequest() request.open('POST', "../php/group.php") request.onload = function() { if (request.status === 200) { console.log(request.responseText) const json = JSON.parse(request.responseText) if (json.status == "success") { var tip = dialog({ title: "提示", content: "回复成功", onclose: function() { window.location.reload() } }) tip.show() } else { var tip = dialog({ title: "提示", content: json.message, }) tip.show() } } } request.send(formData) }) } </script> </body> </html>
浙公网安备 33010602011771号