diff --git a/discussion.php b/discussion.php index 235c28b..3eddbf3 100644 --- a/discussion.php +++ b/discussion.php @@ -154,7 +154,7 @@ function formatDate($id) {
我的
海水
模型库
-
通知
+
通知
'; + exit(); + } +} + +// 检查session数据 +$dt = null; +if (isset($_SESSION['responseBody'])) { + $dt = is_string($_SESSION['responseBody']) ? json_decode($_SESSION['responseBody'], true) : $_SESSION['responseBody']; +} else { + $redirectUrl = 'getv.php?r=nof.php'; + if (!headers_sent()) { + header('Location: ' . $redirectUrl); + exit; + } else { + echo ''; + exit; + } +} + +// 检查登录状态 +$isLoggedIn = isset($dt['Data']['User']['Nickname']) && $dt['Data']['User']['Nickname'] !== '点击登录'; + +// 调用API函数 +function callAPI($endpoint, $method = 'GET', $data = [], $isRetry = false) { + $url = API_BASE_URL . ltrim($endpoint, '/'); + + $headers = [ + 'X-API-Token: ' . $_SESSION['token'], + 'X-API-AuthCode: ' . $_SESSION['authCode'], + 'Content-Type: application/json', + 'Accept: application/json' + ]; + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + if ($method === 'POST') { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } elseif ($method === 'PUT') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + } elseif ($method === 'DELETE') { + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + + curl_close($ch); + + if ($error) { + return [ + 'Status' => 500, + 'Message' => 'CURL Error: ' . $error, + 'Data' => null + ]; + } + + $result = json_decode($response, true); + + // 如果token过期,尝试刷新 + if ($httpCode === 401 && !$isRetry) { + $refreshResult = refreshToken(); + if ($refreshResult['success']) { + return callAPI($endpoint, $method, $data, true); + } + } + + return [ + 'Status' => $httpCode, + 'Message' => $result['Message'] ?? ($httpCode === 200 ? 'Success' : 'Unknown error'), + 'Data' => $result['Data'] ?? $result + ]; +} + +// 刷新token +function refreshToken() { + if (!isset($_SESSION['refreshToken'])) { + return ['success' => false]; + } + + $data = [ + 'refreshToken' => $_SESSION['refreshToken'] + ]; + + $response = callAPI('/Auth/RefreshToken', 'POST', $data); + + if ($response['Status'] === 200) { + $_SESSION['token'] = $response['Data']['Token']; + $_SESSION['authCode'] = $response['Data']['AuthCode']; + if (isset($response['Data']['RefreshToken'])) { + $_SESSION['refreshToken'] = $response['Data']['RefreshToken']; + } + return ['success' => true]; + } + + return ['success' => false]; +} + +// 获取头像URL - 修复avatarNumber问题 +function getAvatarUrl($userID, $avatarNumber = 0) { + if (empty($userID) || strlen($userID) < 24) { + return ''; + } + + $part1 = substr($userID, 0, 4); + $part2 = substr($userID, 4, 2); + $part3 = substr($userID, 6, 2); + $part4 = substr($userID, 8, 16); + + return "http://netlogo-cn.oss-cn-hongkong.aliyuncs.com/users/avatars/{$part1}/{$part2}/{$part3}/{$part4}/{$avatarNumber}.jpg!full"; +} + +// 获取消息列表 - 一次加载100条 +function getMessages($categoryID, $skip = 0, $take = 20, $noTemplates = false) { + $data = [ + 'CategoryID' => $categoryID, + 'Skip' => $skip, + 'Take' => $take, + 'NoTemplates' => $noTemplates + ]; + + return callAPI('/Messages/GetMessages', 'POST', $data); +} + +// 转换函数 +function convertUIIndexToCategoryID($n) { + return $n === 3 ? 2 : ($n === 2 ? 3 : $n); +} + +function convertCategoryIDToUIIndex($n) { + return $n === 2 ? 3 : ($n === 3 ? 2 : $n); +} + +// 模板填充函数 +function fillInTemplate($data, $message, $userInfo) { + if (empty($data)) return ''; + + // 替换用户标记 + if (strpos($data, '{Users}') !== false) { + $usersHtml = ''; + if (isset($message['Users']) && isset($message['UserNames'])) { + foreach ($message['Users'] as $index => $userId) { + $userName = $message['UserNames'][$index] ?? ''; + $usersHtml .= '' . + htmlspecialchars($userName) . ' '; + } + } + $data = str_replace('{Users}', $usersHtml, $data); + } + + // 替换其他标记 + $replacements = [ + '{$TargetName}' => $message['Fields']['TargetName'] ?? $userInfo['nickName'] ?? '', + '{$Content}' => $message['Fields']['Content'] ?? '', + '{$Until}' => $message['Fields']['Unitl'] ?? '', + '{$Editor}' => $message['Fields']['Editor'] ?? '', + '{$Gold}' => $message['Numbers']['Gold'] ?? '', + '{Experiment}' => isset($message['Fields']['Discussion']) ? + '' . + htmlspecialchars($message['Fields']['Discussion']) . '' : (isset($message['Fields']['Experiment']) ? + '' . + htmlspecialchars($message['Fields']['Experiment'] ?? '') . '' : + '' . + htmlspecialchars($message['Fields']['Model'] ?? '') . '') + ]; + + foreach ($replacements as $search => $replace) { + $data = str_replace($search, $replace, $data); + } + + // 清理undefined + $data = str_replace('undefined', '', $data); + + return $data; +} + +// 自定义标签解析器类 +class CustomTagParser { + + /** + * 解析包含自定义标签的文本 + */ + public static function parse(string $text): string { + if (empty($text)) { + return ''; + } + + // 先转义HTML特殊字符,防止XSS攻击 + $text = htmlspecialchars($text); + + // 先解析Markdown标题(在换行转换之前) + $text = self::parseMarkdownHeadings($text); + + // 将换行符转换为
+ $text = nl2br($text); + + // 解析自定义标签 + $patterns = [ + // user标签 - 保持原有格式 + '/<user=([a-f0-9]+)>(.*?)<\/user>/i', + // experiment标签 - 跳转链接 + '/<experiment=([a-f0-9]+)>(.*?)<\/experiment>/i', + // discussion标签 - 跳转链接 + '/<discussion=([a-f0-9]+)>(.*?)<\/discussion>/i', + // model标签 - 跳转链接 + '/<model=([a-f0-9]+)>(.*?)<\/model>/i', + // external标签 - 外部链接 + '/<external=([^&]+)>(.*?)<\/external>/i', + // size标签 + '/<size=([^&]+)>(.*?)<\/size>/i', + // color标签 - 新增 + '/<color=([^&]+)>(.*?)<\/color>/i', + // b标签 + '/<b>(.*?)<\/b>/i', + // i标签 + '/<i>(.*?)<\/i>/i', + // a标签 - 深蓝色标签 + '/<a>(.*?)<\/a>/i' + ]; + + $replacements = [ + // user标签 + '$2', + // experiment标签 + '$2', + // discussion标签 + '$2', + // model标签 + '$2', + // external标签 + '$2', + // size标签 + '$2', + // color标签 - 新增 + '$2', + // b标签 + '$1', + // i标签 + '$1', + // a标签 + '$1' + ]; + + $text = preg_replace($patterns, $replacements, $text); + + // 解析简单的Markdown格式 + $text = self::parseSimpleMarkdown($text); + + return $text; + } + + /** + * 解析Markdown标题 + */ + private static function parseMarkdownHeadings(string $text): string { + // 支持1-6级标题 + // 一级标题: # 标题 + $text = preg_replace('/^# (.+)$/m', '$1', $text); + + // 二级标题: ## 标题 + $text = preg_replace('/^## (.+)$/m', '$1', $text); + + // 三级标题: ### 标题 + $text = preg_replace('/^### (.+)$/m', '$1', $text); + + // 四级标题: #### 标题 + $text = preg_replace('/^#### (.+)$/m', '$1', $text); + + return $text; + } + + /** + * 解析简单的Markdown格式 + */ + private static function parseSimpleMarkdown(string $text): string { + // 粗体 + $text = preg_replace('/\*\*(.*?)\*\*/', '$1', $text); + + // 斜体 + $text = preg_replace('/\*(.*?)\*/', '$1', $text); + + // 删除线 + $text = preg_replace('/~~(.*?)~~/', '$1', $text); + + // 行内代码 + $text = preg_replace('/`([^`]+)`/', '$1', $text); + + return $text; + } +} + +// 根据消息类型获取图标URL +function getMsgIconUrl($msgType) { + $icons = [ + 1 => 'notifications_system.png', + 2 => 'notifications_comments.png', + 3 => 'notifications_followers.png', + 4 => 'notifications_projects.png', + 5 => 'notifications_admin.png' + ]; + + $icon = $icons[$msgType] ?? 'notifications_system.png'; + return "/assets/icons/{$icon}"; +} + +// 获取路径 +function getPath($path) { + $baseUrl = rtrim($_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . dirname($_SERVER['PHP_SELF']), '/'); + + if (strpos($path, '/@base/') === 0) { + return $baseUrl . '/assets' . substr($path, 7); + } elseif (strpos($path, '/@root/') === 0) { + return $baseUrl . substr($path, 7); + } else { + return $baseUrl . '/' . ltrim($path, '/'); + } +} + +// 获取消息类型名称 +function getMsgTypeName($msgType) { + $names = [ + 1 => '系统', + 2 => '评论', + 3 => '关注', + 4 => '作品', + 5 => '管理' + ]; + return $names[$msgType] ?? '消息'; +} + +// 根据ID计算相对时间 +function formatTimeAgo($id) { + if (!empty($id) && strlen($id) >= 8) { + try { + $timestamp = hexdec(substr($id, 0, 8)) * 1000; + $now = time() * 1000; + $diff = $now - $timestamp; + + if ($diff < 60000) return '刚刚'; + elseif ($diff < 3600000) return floor($diff / 60000) . '分钟前'; + elseif ($diff < 86400000) return floor($iff / 3600000) . '小时前'; + elseif ($diff < 604800000) return floor($diff / 86400000) . '天前'; + else return date('Y-m-d', $timestamp / 1000); + } catch (Exception $e) { + return '刚刚'; + } + } + return '刚刚'; +} + +// 渲染单个通知项 +function renderNotificationItem($item) { + // 头像URL - 从消息中获取avatarNumber + $avatarNumber = isset($item['avatar_number']) ? $item['avatar_number'] : 0; + $avatarUrl = ''; + if (!empty($item['uid'])) { + $avatarUrl = getAvatarUrl($item['uid'], $avatarNumber); + } + + // 默认头像 + $defaultAvatar = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSIjRjBGMEYwIi8+CjxwYXRoIGQ9Ik0zMCAzNEMzMy4zMTM3IDM0IDM2IDMxLjMxMzcgMzYgMjhDMzYgMjQuNjg2MyAzMy4zMTM3IDIyIDMwIDIyQzI2LjY4NjMgMjIgMjQgMjQuNjg2MyAyNCAyOEMyNCAzMS4zMTM3IDI2LjY4NjMgMzQgMzAgMzRaIiBmaWxsPSIjQ0VDRUNFIi8+CjxwYXRoIGQ9Ik0zNiAzOEgyNEMyMi44OTU0IDM4IDIyIDM3LjEwNDYgMjIgMzZWMjRDMjIgMjIuODk1NCAyMi44OTU0IDIyIDI0IDIySDM2QzM3LjEwNDYgMjIgMzggMjIuODk1NCAzOCAyNFYzNkMzOCAzNy4xMDQ2IDM3LjEwNDYgMzggMzYgMzhaTTI0IDI0VjM2SDM2VjI0SDI0WiIgZmlsbD0iI0NFQ0VDRSIvPgo8L3N2Zz4K'; + + // 图标URL + $iconUrl = getMsgIconUrl($item['msg_type']); + $typeName = getMsgTypeName($item['msg_type']); + + // 格式化时间 + $timeAgo = formatTimeAgo($item['id']); + + // 点击跳转URL + $onclick = ''; + if ($item['msg_type'] === 2 && !empty($item['tid'])) { + $onclick = 'onclick="window.location.href=\'med.php?category=' . htmlspecialchars($item['category']) . '&id=' . htmlspecialchars($item['tid']) . '\'"'; + $style = 'style="cursor: pointer;"'; + } else { + $style = 'style="cursor: default;"'; + } + + $html = '
'; + + // 头像 + $html .= '头像'; + + // 内容 + $html .= '
'; + $html .= '
' . $item['msg_title'] . '
'; + $html .= '
' . $item['msg'] . '
'; + $html .= ''; + $html .= '
'; + $html .= '
'; + + return $html; +} + +// 处理分页加载 - 一次加载100条 +function getPagedMessages($categoryID, $skip = 0, $take = 20) { + $response = getMessages($categoryID, $skip, $take, false); + + $items = []; + if ($response['Status'] === 200) { + $templates = $response['Data']['Templates'] ?? []; + $messages = $response['Data']['Messages'] ?? []; + + // 用户信息 + global $dt; + $userInfo = $dt['Data']['User'] ?? []; + + foreach ($messages as $message) { + $template = null; + foreach ($templates as $t) { + if ($t['ID'] === $message['TemplateID']) { + $template = $t; + break; + } + } + + if (!$template) continue; + + $locale = 'Chinese'; // 默认为中文 + + $msgTitle = fillInTemplate($template['Subject'][$locale] ?? $template['Subject']['Chinese'], $message, $userInfo); + $msgContent = fillInTemplate($template['Content'][$locale] ?? $template['Content']['Chinese'], $message, $userInfo); + + $items[] = [ + 'id' => $message['ID'], + 'msg_title' => CustomTagParser::parse($msgTitle), + 'msg' => CustomTagParser::parse($msgContent), + 'msg_type' => convertCategoryIDToUIIndex($message['CategoryID']), + 'category' => isset($message['Fields']['User']) ? 'User' : + (isset($message['Fields']['Discussion']) ? 'Discussion' : 'Experiment'), + 'tid' => $message['Fields']['UserID'] ?? + $message['Fields']['DiscussionID'] ?? + $message['Fields']['ExperimentID'] ?? '', + 'name' => $message['Fields']['Discussion'] ?? + $message['Fields']['Experiment'] ?? + $message['Fields']['User'] ?? '', + 'uid' => $message['Users'][0] ?? '', + 'avatar_number' => $message['AvatarNumber'] ?? 0 // 从消息中获取avatarNumber + ]; + } + } + + return $items; +} + +// 处理不同的请求类型 +$action = $_GET['action'] ?? ''; +$notificationTypeIndexOfUI = $_GET['type'] ?? 0; + +// 检查认证(API请求除外) +if ($action !== 'api' && $action !== 'getMessages' && $action !== 'getUserInfo') { + checkAuth(); +} + +// 获取初始消息 - 一次加载100条 +$initialItems = []; +if ($isLoggedIn) { + $initialItems = getPagedMessages(convertUIIndexToCategoryID($notificationTypeIndexOfUI), 0, 20); +} + +// 主页面显示 +if ($action === '') { + displayNotificationsPage(); + exit; +} + +// API代理处理函数 +function handleApiRequest() { + $endpoint = $_GET['endpoint'] ?? ''; + $method = $_SERVER['REQUEST_METHOD']; + + if (empty($endpoint)) { + http_response_code(400); + echo json_encode(['error' => 'Endpoint is required']); + return; + } + + $data = []; + if ($method === 'POST' || $method === 'PUT') { + $input = file_get_contents('php://input'); + $data = json_decode($input, true) ?: []; + } + + $response = callAPI($endpoint, $method, $data); + + header('Content-Type: application/json'); + echo json_encode($response); +} + +// 获取消息处理函数 +function handleGetMessages() { + $categoryID = $_POST['CategoryID'] ?? convertUIIndexToCategoryID($_GET['type'] ?? 0); + $skip = $_POST['Skip'] ?? 0; + $take = $_POST['Take'] ?? 100; // 改为100条 + + $response = getMessages($categoryID, $skip, $take, false); + + if ($response['Status'] === 200) { + // 处理消息数据 + global $dt; + $userInfo = $dt['Data']['User'] ?? []; + $templates = $response['Data']['Templates'] ?? []; + $messages = $response['Data']['Messages'] ?? []; + + $processedMessages = []; + foreach ($messages as $message) { + $template = null; + foreach ($templates as $t) { + if ($t['ID'] === $message['TemplateID']) { + $template = $t; + break; + } + } + + if (!$template) continue; + + $locale = 'Chinese'; + + $msgTitle = fillInTemplate($template['Subject'][$locale] ?? $template['Subject']['Chinese'], $message, $userInfo); + $msgContent = fillInTemplate($template['Content'][$locale] ?? $template['Content']['Chinese'], $message, $userInfo); + + $processedMessages[] = [ + 'id' => $message['ID'], + 'msg_title' => CustomTagParser::parse($msgTitle), + 'msg' => CustomTagParser::parse($msgContent), + 'msg_type' => convertCategoryIDToUIIndex($message['CategoryID']), + 'category' => isset($message['Fields']['User']) ? 'User' : + (isset($message['Fields']['Discussion']) ? 'Discussion' : 'Experiment'), + 'tid' => $message['Fields']['UserID'] ?? + $message['Fields']['DiscussionID'] ?? + $message['Fields']['ExperimentID'] ?? '', + 'name' => $message['Fields']['Discussion'] ?? + $message['Fields']['Experiment'] ?? + $message['Fields']['User'] ?? '', + 'uid' => $message['Users'][0] ?? '', + 'avatar_number' => $message['AvatarNumber'] ?? 0 + ]; + } + + $response['Data']['ProcessedMessages'] = $processedMessages; + } + + header('Content-Type: application/json'); + echo json_encode($response); +} + +// 获取用户信息处理函数 +function handleGetUserInfo() { + $userId = $_GET['userId'] ?? null; + + if (!$userId) { + echo json_encode(['Status' => 400, 'Message' => '用户ID不能为空']); + return; + } + + $response = callAPI('/Users/GetUserInfo?userId=' . urlencode($userId)); + + header('Content-Type: application/json'); + echo json_encode($response); +} + +// 显示通知页面 +function displayNotificationsPage() { + global $dt, $notificationTypeIndexOfUI, $isLoggedIn, $initialItems; + + // 输出HTML页面 +?> + + + + + + 通知 - Turtle Universe Web + + + + +
+

通知

+
+ + + +
+ +
+ +

请先登录

+

登录后查看通知消息

+ +
+ +
+ +

暂无通知

+

还没有收到任何通知消息

+
+ +
+ + + +
+ 已显示全部通知 +
+
+ +
+ + + + + + + diff --git a/styles/comment.css b/styles/comment.css index a047e4b..3a6cd62 100644 --- a/styles/comment.css +++ b/styles/comment.css @@ -17,7 +17,7 @@ textarea:focus { } /* 评论框样式修复 */ -dialog { +.dialog { position: fixed; top: 5%; left: 5%; @@ -190,8 +190,8 @@ dialog::backdrop { background: white; border-radius: 15px; padding: 25px; - width: 100%; - max-width: 400px; + width: 120%; + max-width: 600px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; box-sizing: border-box; diff --git a/user/card.php b/user/card.php index 92e6dd7..2376bd5 100644 --- a/user/card.php +++ b/user/card.php @@ -107,8 +107,8 @@ class="avatar" background: white; border-radius: 15px; padding: 25px; - width: 90%; - max-width: 400px; + width: 120%; + max-width: 600px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; box-sizing: border-box;