关于开发社区网站小组发帖功能和多级回复的代码
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><?phpinclude "../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").valuevar content = document.getElementById("content").valuevar 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;// 返回 JSONecho 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()}// 辅助函数防XSSfunction 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 = truesubmitBtn.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 = truesubmitBtn.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>
