Deprecated: Creation of dynamic property Typecho\Widget\Request::$feed is deprecated in /www/wwwroot/blog.iletter.top/var/Widget/Archive.php on line 246
白荼日记 - PHP https://blog.iletter.top/index.php/tag/PHP/ typecho给后台admin界面添加图标分析 https://blog.iletter.top/index.php/archives/406.html 2025-07-27T02:32:00+08:00 觉得后台admin空荡荡的,索性加个图表自己魔改一下下。以下是完整的index.php<?php include 'common.php'; include 'header.php'; include 'menu.php'; $stat = \Widget\Stat::alloc(); ?> <div class="main"> <div class="container typecho-dashboard"> <?php include 'page-title.php'; ?> <div class="row typecho-page-main"> <div class="col-mb-12 welcome-board" role="main"> <p><?php _e('目前有 <em>%s</em> 篇文章, 并有 <em>%s</em> 条关于你的评论在 <em>%s</em> 个分类中.', $stat->myPublishedPostsNum, $stat->myPublishedCommentsNum, $stat->categoriesNum); ?> <!--<br><?php _e('点击下面的链接快速开始:'); ?></p>--> <ul id="start-link" class="clearfix"> <?php if ($user->pass('contributor', true)): ?> <li><a href="<?php $options->adminUrl('write-post.php'); ?>"><?php _e('撰写新文章'); ?></a></li> <?php if ($user->pass('editor', true) && 'on' == $request->get('__typecho_all_comments') && $stat->waitingCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=waiting'); ?>"><?php _e('待审核的评论'); ?></a> <span class="balloon"><?php $stat->waitingCommentsNum(); ?></span> </li> <?php elseif ($stat->myWaitingCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=waiting'); ?>"><?php _e('待审核评论'); ?></a> <span class="balloon"><?php $stat->myWaitingCommentsNum(); ?></span> </li> <?php endif; ?> <?php if ($user->pass('editor', true) && 'on' == $request->get('__typecho_all_comments') && $stat->spamCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=spam'); ?>"><?php _e('垃圾评论'); ?></a> <span class="balloon"><?php $stat->spamCommentsNum(); ?></span> </li> <?php elseif ($stat->mySpamCommentsNum > 0): ?> <li> <a href="<?php $options->adminUrl('manage-comments.php?status=spam'); ?>"><?php _e('垃圾评论'); ?></a> <span class="balloon"><?php $stat->mySpamCommentsNum(); ?></span> </li> <?php endif; ?> <?php if ($user->pass('administrator', true)): ?> <li><a href="<?php $options->adminUrl('manage-posts.php'); ?>"><?php _e('文章管理'); ?></a></li> <!--<li><a href="<?php $options->adminUrl('themes.php'); ?>"><?php _e('更换外观'); ?></a></li>--> <li><a href="<?php $options->adminUrl('plugins.php'); ?>"><?php _e('插件管理'); ?></a></li> <li><a href="<?php $options->adminUrl('options-general.php'); ?>"><?php _e('系统设置'); ?></a> </li> <?php endif; ?> <?php endif; ?> <!--<li><a href="<?php $options->adminUrl('profile.php'); ?>"><?php _e('更新我的资料'); ?></a></li>--> </ul> </div> <div class="col-mb-12 col-tb-4" role="complementary"> <section class="latest-link"> <h3><?php _e('最近发布的文章'); ?></h3> <?php \Widget\Contents\Post\Recent::alloc('pageSize=10')->to($posts); ?> <ul> <?php if ($posts->have()): ?> <?php while ($posts->next()): ?> <li> <span><?php $posts->date('n.j'); ?></span> <a href="<?php $posts->permalink(); ?>" class="title"><?php $posts->title(); ?></a> </li> <?php endwhile; ?> <?php else: ?> <li><em><?php _e('暂时没有文章'); ?></em></li> <?php endif; ?> </ul> </section> </div> <div class="col-mb-12 col-tb-4" role="complementary"> <section class="latest-link"> <h3><?php _e('最近得到的回复'); ?></h3> <ul> <?php \Widget\Comments\Recent::alloc('pageSize=10')->to($comments); ?> <?php if ($comments->have()): ?> <?php while ($comments->next()): ?> <li> <span><?php $comments->date('n.j'); ?></span> <a href="<?php $comments->permalink(); ?>" class="title"><?php $comments->author(false); ?></a>: <?php $comments->excerpt(35, '...'); ?> </li> <?php endwhile; ?> <?php else: ?> <li><?php _e('暂时没有回复'); ?></li> <?php endif; ?> </ul> </section> </div> <!--<div class="col-mb-12 col-tb-4" role="complementary">--> <!-- <section class="latest-link">--> <!-- <h3><?php _e('官方最新日志'); ?></h3>--> <!-- <div id="typecho-message">--> <!-- <ul>--> <!-- <li><?php _e('读取中...'); ?></li>--> <!-- </ul>--> <!-- </div>--> <!-- </section>--> <!--</div>--> <!-- 图表显示区域 --> <div class="col-mb-12 welcome-board"> <h3 style="color:black"><?php _e('文章统计数据'); ?></h3> <div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px;"> <!-- 月度发布图表 --> <div style="flex: 1; min-width: 400px;"> <canvas id="monthlyPostChart" height="300"></canvas> </div> <!-- 状态分布图表 --> <div style="flex: 1; min-width: 300px;"> <canvas id="statusChart" height="300"></canvas> </div> </div> <div style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 20px;"> <!-- 分类文章统计 --> <div style="flex: 1; min-width: 400px;"> <canvas id="categoryChart" height="300"></canvas> </div> <!-- 标签文章统计 --> <div style="flex: 1; min-width: 400px;"> <canvas id="tagChart" height="300"></canvas> </div> </div> </div> </div> </div> </div> <?php include 'copyright.php'; include 'common-js.php'; ?> <script src="https://blog.iletter.top/usr/blog_img/chart.js"></script> <script> $(document).ready(function() { <?php // ==================== PHP 数据计算部分 ==================== // 1. 月度文章统计数据 $db = Typecho_Db::get(); // 月度文章统计 $select = $db->select() ->from('table.contents') ->where('type = ?', 'post') ->where('status = ?', 'publish'); $posts = $db->fetchAll($select); $monthlyData = []; $monthLabels = []; $currentYear = date('Y'); $currentMonth = date('n'); // 初始化最近12个月的数据 for ($i = 11; $i >= 0; $i--) { $year = $currentYear; $month = $currentMonth - $i; if ($month <= 0) { $month += 12; $year--; } $key = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT); $monthlyData[$key] = 0; $monthLabels[$key] = $year . '年' . $month . '月'; } // 统计实际数据 foreach ($posts as $post) { $created = $post['created']; $year = date('Y', $created); $month = date('m', $created); $key = $year . '-' . $month; if (isset($monthlyData[$key])) { $monthlyData[$key]++; } } // 2. 文章状态统计 $published = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.contents') ->where('type = ?', 'post') ->where('status = ?', 'publish'))->count; $draft = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.contents') ->where('type = ?', 'post_draft') ->where('status = ?', 'publish'))->count; // 3. 分类文章统计 $categories = $db->fetchAll($db->select()->from('table.metas')->where('type = ?', 'category')); $categoryData = []; $categoryLabels = []; foreach ($categories as $category) { $count = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.relationships') ->where('mid = ?', $category['mid']))->count; if ($count > 0) { // 只显示有文章的分类 $categoryLabels[] = $category['name']; $categoryData[] = (int)$count; } } // 4. 标签文章统计 (取前10个) $tags = $db->fetchAll($db->select()->from('table.metas')->where('type = ?', 'tag')); $tagData = []; $tagLabels = []; foreach ($tags as $tag) { $count = $db->fetchObject($db->select(['COUNT(*)' => 'count']) ->from('table.relationships') ->where('mid = ?', $tag['mid']))->count; if ($count > 0) { $tagLabels[] = $tag['name']; $tagData[] = (int)$count; } } // 按数量排序,取前10个 array_multisort($tagData, SORT_DESC, $tagLabels); $topTagLabels = array_slice($tagLabels, 0, 10); $topTagData = array_slice($tagData, 0, 10); // 准备图表数据 $chartMonthlyData = [ 'labels' => array_values($monthLabels), 'values' => array_values($monthlyData) ]; $chartStatusData = [ 'published' => (int)$published, 'draft' => (int)$draft ]; $chartCategoryData = [ 'labels' => $categoryLabels, 'values' => $categoryData ]; $chartTagData = [ 'labels' => $topTagLabels, 'values' => $topTagData ]; ?> // ==================== JavaScript 图表渲染部分 ==================== // 1. 月度文章图表 var monthlyData = <?php echo json_encode($chartMonthlyData); ?>; if (monthlyData.labels && monthlyData.labels.length > 0) { var ctx1 = document.getElementById('monthlyPostChart').getContext('2d'); new Chart(ctx1, { type: 'line', data: { labels: monthlyData.labels, datasets: [{ label: '每月发文数', data: monthlyData.values, borderColor: 'rgb(75, 192, 192)', backgroundColor: 'rgba(75, 192, 192, 0.2)', tension: 0.1, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }, plugins: { title: { display: true, text: '月度文章发布趋势' } } } }); } // 2. 文章状态图表 var statusData = <?php echo json_encode($chartStatusData); ?>; if (statusData) { var ctx2 = document.getElementById('statusChart').getContext('2d'); new Chart(ctx2, { type: 'doughnut', data: { labels: ['已发布', '草稿'], datasets: [{ data: [statusData.published, statusData.draft], backgroundColor: [ 'rgba(75, 192, 192, 0.8)', 'rgba(255, 205, 86, 0.8)', ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '文章状态分布' }, legend: { position: 'bottom' } } } }); } // 3. 分类文章图表 var categoryData = <?php echo json_encode($chartCategoryData); ?>; if (categoryData.labels && categoryData.labels.length > 0) { var ctx3 = document.getElementById('categoryChart').getContext('2d'); new Chart(ctx3, { type: 'bar', data: { labels: categoryData.labels, datasets: [{ label: '文章数量', data: categoryData.values, backgroundColor: 'rgba(153, 102, 255, 0.8)', borderColor: 'rgba(153, 102, 255, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }, plugins: { title: { display: true, text: '各分类文章数量' } } } }); } // 4. 标签文章图表 var tagData = <?php echo json_encode($chartTagData); ?>; if (tagData.labels && tagData.labels.length > 0) { var ctx4 = document.getElementById('tagChart').getContext('2d'); new Chart(ctx4, { type: 'bar', data: { labels: tagData.labels, datasets: [{ label: '文章数量', data: tagData.values, backgroundColor: 'rgba(255, 159, 64, 0.8)', borderColor: 'rgba(255, 159, 64, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }, plugins: { title: { display: true, text: '热门标签文章数量 (前10)' } } } }); } }); </script> <script> $(document).ready(function () { var ul = $('#typecho-message ul'), cache = window.sessionStorage, html = cache ? cache.getItem('feed') : '', update = cache ? cache.getItem('update') : ''; if (!!html) { ul.html(html); } else { html = ''; $.get('<?php $options->index('/action/ajax?do=feed'); ?>', function (o) { for (var i = 0; i < o.length; i++) { var item = o[i]; html += '<li><span>' + item.date + '</span> <a href="' + item.link + '" target="_blank">' + item.title + '</a></li>'; } ul.html(html); cache.setItem('feed', html); }, 'json'); } function applyUpdate(update) { if (update.available) { $('<div class="update-check message error"><p>' + '<?php _e('您当前使用的版本是 %s'); ?>'.replace('%s', update.current) + '<br />' + '<strong><a href="' + update.link + '" target="_blank">' + '<?php _e('官方最新版本是 %s'); ?>'.replace('%s', update.latest) + '</a></strong></p></div>') .insertAfter('.typecho-page-title').effect('highlight'); } } if (!!update) { applyUpdate($.parseJSON(update)); } else { $.get('<?php $options->index('/action/ajax?do=checkVersion'); ?>', function (o, status, resp) { applyUpdate(o); cache.setItem('update', resp.responseText); }, 'json'); } }); </script> <?php include 'footer.php'; ?> 其他插件推荐:[post cid="405" /] 为typecho博客添加Gotify插件通知 https://blog.iletter.top/index.php/archives/405.html 2025-07-27T02:05:00+08:00 受限于typecho博客没有通知,自己写了一个博客有评论就通知的gotify插件,脚本使用之前的python改的。这是效果<?php namespace TypechoPlugin\GotifyNotify; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Typecho\Widget\Helper\Form\Element\Text; // use Typecho\Widget\Helper\Form\Element\Radio; // 不再需要 Radio 元素 use Widget\Options; use Typecho\Plugin\Exception as PluginException; use Typecho\Widget\Exception as WidgetException; use Typecho\Db\Exception as DbException; use Utils; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * 当用户评论时通过 Gotify 发送通知 * * @package GotifyNotify * @author 白荼 * @version 1.3.0 * @link https://gotify.net */ class Plugin implements PluginInterface { /** * 激活插件方法 * * @access public * @return void * @throws PluginException */ public static function activate() { // 监听评论完成事件 - 触发服务调用 \Typecho\Plugin::factory('Widget_Feedback')->finishComment = __CLASS__ . '::requestService'; \Typecho\Plugin::factory('Widget_Comments_Edit')->finishComment = __CLASS__ . '::requestService'; // 注册异步调用服务 \Typecho\Plugin::factory('Widget_Service')->sendGotify = __CLASS__ . '::sendGotify'; } /** * 禁用插件方法 * * @access public * @return void * @throws PluginException */ public static function deactivate() { // 通常不需要显式移除 } /** * 获取插件配置面板 * * @param Form $form 配置面板 * @access public * @return void */ public static function config(Form $form) { // Gotify 服务器地址 $serverUrl = new Text('serverUrl', NULL, '', _t('Gotify 服务器地址'), _t('例如: http://your-gotify-server.com')); $form->addInput($serverUrl->addRule('required', _t('Gotify 服务器地址不能为空'))); // 应用 Token $appToken = new Text('appToken', NULL, '', _t('应用 Token'), _t('在 Gotify 中创建应用后获得的 Token')); $form->addInput($appToken->addRule('required', _t('应用 Token 不能为空'))); // 通知标题 $title = new Text('title', NULL, '博客有新评论', _t('通知标题'), _t('收到新评论时的推送标题')); $form->addInput($title); // 消息优先级 (Priority) - 使用 Text 输入框 $priority = new Text('priority', NULL, '1', _t('消息优先级'), _t('设置 Gotify 消息的优先级 (自己在Gorify的定义)')); $form->addInput($priority); // 移除了 "是否启用" 的配置选项,插件默认启用 } /** * 个人用户的配置面板 * * @param Form $form * @access public * @return void */ public static function personalConfig(Form $form) { // 个人配置通常为空 } /** * 评论通知回调 - 触发异步服务 * 这个方法在评论完成后被调用 * * @access public * @param $comment (Widget_Comments_Edit 或 Widget_Feedback 的实例) * @return void * @throws PluginException */ public static function requestService($comment) { // 检查评论对象是否有效 if (!isset($comment->coid) || !$comment->have()) { error_log("GotifyNotify: Invalid comment object received in requestService."); return; } $coid = $comment->coid; $options = Options::alloc()->plugin('GotifyNotify'); // 移除了对 $options->enable 的检查,插件默认启用 // 检查必要配置 if (empty($options->serverUrl) || empty($options->appToken)) { error_log("GotifyNotify: Missing server URL or app token, skipping notification for comment ID {$coid}."); return; } // 调用 Widget_Service 中注册的异步方法 // 将评论 ID 传递给异步服务 try { error_log("GotifyNotify: Calling async service for comment ID {$coid}."); Utils\Helper::requestService('sendGotify', $coid); } catch (\Exception $e) { error_log("GotifyNotify: Failed to call async service for comment ID {$coid}. Error: " . $e->getMessage()); } } /** * 异步发送 Gotify 通知 * 这个方法由 Widget_Service 调用 * * @param integer $coid 评论ID * @access public * @return void * @throws WidgetException * @throws DbException */ public static function sendGotify(int $coid) { error_log("GotifyNotify Service: Starting to process comment ID {$coid}."); // 获取插件配置 $options = Options::alloc()->plugin('GotifyNotify'); // 移除了对 $options->enable 的检查,插件默认启用 // 重新获取评论对象 try { $commentWidget = Utils\Helper::widgetById('comments', $coid); } catch (WidgetException $e) { error_log("GotifyNotify Service: Failed to get comment widget for ID {$coid}. Error: " . $e->getMessage()); return; } if (!$commentWidget->have()) { error_log("GotifyNotify Service: Comment widget is empty for ID {$coid}."); return; } // 检查必要配置 if (empty($options->serverUrl) || empty($options->appToken)) { error_log("GotifyNotify Service: Missing configuration for comment ID {$coid}."); return; } try { // 构造通知内容 $title = $options->title ?: '博客有新评论'; $message = self::buildMessage($commentWidget); // 传入 widget 对象 // 获取优先级设置 $priority = $options->priority ?? '1'; // 默认优先级为 1 error_log("GotifyNotify Service: Prepared message for comment ID {$coid}. Title: {$title}, Priority: {$priority}"); // 发送通知,传入优先级 self::sendGotifyMessage($options->serverUrl, $options->appToken, $title, $message, $priority); error_log("GotifyNotify Service: Successfully sent notification for comment ID {$coid}."); } catch (\Exception $e) { // 记录详细错误信息 error_log("GotifyNotify Service: Failed to send notification for comment ID {$coid}. Error: " . $e->getMessage()); error_log("GotifyNotify Service: Stack trace: " . $e->getTraceAsString()); } } /** * 构造通知消息内容 * * @param \Widget\Base\Comments $comment 评论对象 * @return string 消息内容 */ private static function buildMessage($comment) { $content = ''; if (is_object($comment) && $comment->have()) { // 从评论对象获取信息 $author = $comment->author ?? '匿名用户'; $mail = $comment->mail ?? ''; $url = $comment->url ?? ''; $text = $comment->text ?? ''; // $ip = $comment->ip ?? ''; // Widget\Base\Comments 通常不直接暴露 IP $content = "作者: {$author}\n"; if (!empty($mail)) { $content .= "邮箱: {$mail}\n"; } if (!empty($url)) { $content .= "网站: {$url}\n"; } // if (!empty($ip)) { // $content .= "IP: {$ip}\n"; // } $content .= "内容: {$text}\n"; $content .= "时间: " . date('Y-m-d H:i:s', $comment->created) . "\n"; // 使用评论创建时间 $content .= "文章: " . $comment->title . "\n"; // 文章标题 $content .= "链接: " . $comment->permalink . "\n"; // 评论链接 } else { // 兼容其他情况 $content = "收到新评论,请登录后台查看 (评论ID: " . ($comment->coid ?? 'unknown') . ")"; } return $content; } /** * 发送 Gotify 消息 * * @param string $serverUrl Gotify 服务器地址 * @param string $appToken 应用 Token * @param string $title 消息标题 * @param string $message 消息内容 * @param string $priority 消息优先级 * @throws \Exception */ private static function sendGotifyMessage($serverUrl, $appToken, $title, $message, $priority = '1') { // 清理 URL $serverUrl = rtrim($serverUrl, '/'); // 构造请求 URL (使用查询参数传递 token) $url = $serverUrl . '/message'; // 准备表单数据,包含 priority $data = [ 'title' => $title, 'message' => $message, 'priority' => $priority // 使用传入的优先级 ]; // 准备查询参数 $params = ['token' => $appToken]; $fullUrl = $url . '?' . http_build_query($params); error_log("GotifyNotify: Sending request to {$fullUrl}"); error_log("GotifyNotify: POST data: " . print_r($data, true)); // 使用 cURL 发送请求 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $fullUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // 发送表单数据 curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Accept: application/json' ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 根据你的服务器 SSL 配置调整 curl_setopt($ch, CURLOPT_USERAGENT, 'Typecho GotifyNotify Plugin/1.3.0'); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); error_log("GotifyNotify: cURL Response Code: {$httpCode}"); error_log("GotifyNotify: cURL Response Body: {$response}"); error_log("GotifyNotify: cURL Error (if any): {$error}"); // 检查响应 if ($response === false) { throw new \Exception('cURL error: ' . $error); } if ($httpCode >= 200 && $httpCode < 300) { // 成功 error_log("GotifyNotify: Message sent successfully."); // 可以进一步检查返回的 JSON $result = json_decode($response, true); if (!$result) { error_log("GotifyNotify: Warning - Could not decode JSON response, but HTTP status was OK."); } return; // 成功返回 } else { // 失败 throw new \Exception('HTTP error: ' . $httpCode . ', response: ' . $response); } } }