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/tag/PHP/ typecho博客Hello world插件增强 https://blog.iletter.top/archives/424.html 2025-08-07T13:59:32+08:00 后台点击用户名称旁边的hello word会出现提示。直接将该代码替换掉原本的代码即可:<?php namespace TypechoPlugin\HelloWorld; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Typecho\Widget\Helper\Form\Element\Text; use Widget\Options; use Widget\Stat; use Typecho\Db; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Hello World * * @package HelloWorld * @author 白荼 * @version 1.0.0 */ class Plugin implements PluginInterface { /** * 激活插件方法,如果激活失败,直接抛出异常 */ public static function activate() { \Typecho\Plugin::factory('admin/menu.php')->navBar = __CLASS__ . '::render'; // 添加CSS和JS \Typecho\Plugin::factory('admin/header.php')->header = __CLASS__ . '::header'; } /** * 禁用插件方法,如果禁用失败,直接抛出异常 */ public static function deactivate() { } /** * 获取插件配置面板 * * @param Form $form 配置面板 */ public static function config(Form $form) { /** 分类名称 */ $name = new Text('word', null, 'Hello World', _t('说点什么')); $form->addInput($name); } /** * 个人用户的配置面板 * * @param Form $form */ public static function personalConfig(Form $form) { } /** * 在header中添加CSS和JS */ public static function header($header) { $customCssJs = ' <!-- HelloWorld Plugin CSS --> <style> .helloworld-tooltip { position: absolute; background: #fff; border: 1px solid #ddd; border-radius: 4px; padding: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 9999; min-width: 280px; display: none; font-size: 12px; } .helloworld-tooltip h4 { margin: 0 0 10px 0; padding: 0; color: #333; border-bottom: 1px solid #eee; padding-bottom: 8px; font-size: 14px; } .helloworld-tooltip .comparison { background: #f8f9fa; border-left: 3px solid #007cba; padding: 8px 12px; margin: 10px 0; border-radius: 0 4px 4px 0; font-size: 13px; } .helloworld-tooltip ul { list-style: none; padding: 0; margin: 0; } .helloworld-tooltip li { padding: 5px 0; border-bottom: 1px solid #f5f5f5; line-height: 1.5; } .helloworld-tooltip li:last-child { border-bottom: none; } .helloworld-tooltip strong { color: #666; font-weight: normal; } .helloworld-trigger { cursor: pointer; border-bottom: 1px dotted #BBBBBB; } .helloworld-trigger:hover { color: #666 !important; border-bottom-color: #666; } </style> <!-- HelloWorld Plugin JS --> <script> (function() { function initHelloWorldTooltip() { var trigger = document.querySelector(".helloworld-trigger"); var tooltip = document.querySelector(".helloworld-tooltip"); if (trigger && tooltip) { trigger.addEventListener("click", function(e) { e.preventDefault(); e.stopPropagation(); // 计算位置 var rect = trigger.getBoundingClientRect(); var tooltipWidth = tooltip.offsetWidth || 280; var tooltipHeight = tooltip.offsetHeight || 250; // 默认位置 var left = rect.left + window.scrollX; var top = rect.bottom + window.scrollY + 5; // 边界检测 if (left + tooltipWidth > window.innerWidth + window.scrollX) { left = window.innerWidth + window.scrollX - tooltipWidth - 10; } if (top + tooltipHeight > window.innerHeight + window.scrollY) { top = rect.top + window.scrollY - tooltipHeight - 5; } tooltip.style.left = left + "px"; tooltip.style.top = top + "px"; tooltip.style.display = "block"; }); // 点击其他地方关闭 document.addEventListener("click", function(e) { if (!tooltip.contains(e.target) && e.target !== trigger) { tooltip.style.display = "none"; } }); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initHelloWorldTooltip); } else { initHelloWorldTooltip(); } })(); </script>'; return $header . $customCssJs; } /** * 根据字数获取对应的文学著作比较 */ private static function getLiteraryComparison($wordCount) { // 文学著作字数对照表(字数 => 著作名称) $literaryWorks = [ // 百字级 50 => '几行诗句', 100 => '一段朋友圈文字', 200 => '一条微博内容', 500 => '一篇微博长文', // 千字级 1000 => '一页A4纸手写内容', 1500 => '一页打印稿', 2000 => '一篇标准作文', 3000 => '一篇博客文章', 5000 => '一篇短篇小说', 8000 => '一篇中篇小说开头', // 万字级(中国现代文学) 10000 => '《呐喊》', 12000 => '《彷徨》', 15000 => '《朝花夕拾》', 20000 => '《野草》', 25000 => '《故事新编》', 30000 => '《热风》', 35000 => '《坟》', 40000 => '《而已集》', 45000 => '《三闲集》', // 万字级(其他现代文学) 50000 => '《沉沦》(郁达夫)', 55000 => '《春风沉醉的晚上》', 60000 => '《迟桂花》', 65000 => '《茑萝集》', 70000 => '《过去》', 75000 => '《薄奠》', 80000 => '《微雪的早晨》', // 万字级(长篇小说选段) 85000 => '《骆驼祥子》前半部分', 90000 => '《二马》', 95000 => '《离婚》', 100000 => '《骆驼祥子》', 110000 => '《四世同堂》第一部分', 120000 => '《边城》', 130000 => '《湘行散记》', 140000 => '《长河》', 150000 => '《家》', 160000 => '《憩园》', 170000 => '《第四病室》', 180000 => '《春》', 190000 => '《秋》', 200000 => '《寒夜》', // 20万字级 210000 => '《激流三部曲》选段', 220000 => '《爱情三部曲》选段', 230000 => '《抗战三部曲》选段', 240000 => '《人间词话》', 250000 => '《人间喜剧》选段', 260000 => '《巴黎圣母院》', 270000 => '《悲惨世界》选段', 280000 => '《九三年》', 290000 => '《海上劳工》', 300000 => '《笑面人》', // 30万字级 310000 => '《约翰·克利斯朵夫》选段', 320000 => '《红与黑》', 330000 => '《巴马修道院》', 340000 => '《吕西安·娄凡》', 350000 => '《阿尔芒斯》', 360000 => '《法尼娜·法尼尼》', 370000 => '《瓦尼娜·瓦尼尼》', 380000 => '《红屋骑士》', 390000 => '《卡斯特罗修道院院长》', 400000 => '《意大利遗事》', // 40万字级 410000 => '《三个火枪手》', 420000 => '《基督山伯爵》', 430000 => '《黑郁金香》', 440000 => '《二十年后》', 450000 => '《布拉热洛纳子爵》', 460000 => '《玛戈王后》', 470000 => '《蒙梭罗夫人》', 480000 => '《四十五卫士》', 490000 => '《侠隐记》', 500000 => '《红楼梦》(精简版)', // 50万字级 520000 => '《水浒传》选段', 540000 => '《西游记》选段', 560000 => '《三国演义》选段', 580000 => '《儒林外史》', 600000 => '《聊斋志异》选段', 620000 => '《阅微草堂笔记》', 640000 => '《子不语》', 660000 => '《夜雨秋灯录》', 680000 => '《萤窗异草》', 700000 => '《耳食录》', // 70万字级 720000 => '《浮生六记》', 740000 => '《影梅庵忆语》', 760000 => '《秋灯琐忆》', 780000 => '《香艳丛书》选段', 800000 => '《太平广记》选段', 820000 => '《夷坚志》选段', 840000 => '《剪灯新话》', 860000 => '《剪灯余话》', 880000 => '《觅灯因话》', 900000 => '《喻世明言》', // 百万字级 920000 => '《警世通言》', 940000 => '《醒世恒言》', 960000 => '《初刻拍案惊奇》', 980000 => '《二刻拍案惊奇》', 1000000 => '《红楼梦》', 1050000 => '《镜花缘》', 1100000 => '《老残游记》', 1150000 => '《孽海花》', 1200000 => '《官场现形记》', 1250000 => '《二十年目睹之怪现状》', // 百万字级以上 1300000 => '《资本论》第一卷', 1400000 => '《莎士比亚十四行诗集》', 1500000 => '《战争与和平》', 1600000 => '《安娜·卡列尼娜》', 1700000 => '《复活》', 1800000 => '《静静的顿河》', 1900000 => '《被开垦的处女地》', 2000000 => '《一个人的遭遇》', 2100000 => '《静静的顿河》全本', 2200000 => '《钢铁是怎样炼成的》', 2300000 => '《青年近卫军》', 2400000 => '《日瓦戈医生》', 2500000 => '《追忆似水年华》第一卷', 2600000 => '《约翰·克利斯朵夫》', 2700000 => '《马丁·伊登》', 2800000 => '《美国悲剧》', 2900000 => '《嘉莉妹妹》', 3000000 => '《资本论》全三卷', 3100000 => '《莎士比亚全集》悲剧部分', 3200000 => '《莎士比亚全集》喜剧部分', 3300000 => '《莎士比亚全集》历史剧部分', 3400000 => '《莎士比亚十四行诗全集》', 3500000 => '《莎士比亚全集》', 3600000 => '《追忆似水年华》第二卷', 3700000 => '《追忆似水年华》第三卷', 3800000 => '《追忆似水年华》第四卷', 3900000 => '《追忆似水年华》第五卷', 4000000 => '《追忆似水年华》', 4200000 => '《堂吉诃德》', 4400000 => '《神曲》', 4600000 => '《荷马史诗》', 4800000 => '《失乐园》', 5000000 => '《大英百科全书》一卷', 5500000 => '《大英百科全书》两卷', 6000000 => '《牛津英语词典》一卷', 6500000 => '《不列颠百科全书》', 7000000 => '《中国大百科全书》一卷', 7500000 => '《辞海》', 8000000 => '《康熙字典》', 9000000 => '《中华大字典》', 10000000 => '《大英百科全书》全套', 12000000 => '《四库全书》选段', 15000000 => '《永乐大典》残卷', 20000000 => '《古今图书集成》选段' ]; // 找到最接近的著作 $closestWork = '几行文字'; $closestWords = 0; foreach ($literaryWorks as $words => $work) { if ($wordCount >= $words) { $closestWork = $work; $closestWords = $words; } else { break; } } // 计算倍数 if ($closestWords > 0) { $multiple = round($wordCount / $closestWords, 1); if ($multiple >= 2) { return "约等于 {$multiple} 本 {$closestWork} 的字数"; } else { return "约等于 1 本 {$closestWork} 的字数"; } } else { return "约等于几行文字的字数"; } } /** * 获取统计信息 */ private static function getStats() { $db = Db::get(); $user = \Typecho\Widget::widget('Widget_User'); // 最近登录时间 $lastLogin = $user->logged; $lastLoginText = $lastLogin ? date('Y-m-d H:i:s', $lastLogin) : '未知'; // 文章数量 $postCount = $db->fetchObject($db->select(['COUNT(cid)' => 'num']) ->from('table.contents') ->where('type = ?', 'post') ->where('authorId = ?', $user->uid))->num; // 字数统计(简单统计标题和内容长度) $posts = $db->fetchAll($db->select('title', 'text') ->from('table.contents') ->where('type = ?', 'post') ->where('authorId = ?', $user->uid)); $totalWords = 0; foreach ($posts as $post) { $totalWords += mb_strlen($post['title'], 'UTF-8'); $totalWords += mb_strlen(strip_tags($post['text']), 'UTF-8'); } // 最近文章时间 $lastPost = $db->fetchRow($db->select('created') ->from('table.contents') ->where('type = ?', 'post') ->where('authorId = ?', $user->uid) ->order('created', Db::SORT_DESC) ->limit(1)); $lastPostTime = $lastPost ? date('Y-m-d H:i:s', $lastPost['created']) : '暂无文章'; // 获取文学著作比较 $literaryComparison = self::getLiteraryComparison($totalWords); return [ 'lastLogin' => $lastLoginText, 'postCount' => $postCount, 'totalWords' => $totalWords, 'lastPostTime' => $lastPostTime, 'literaryComparison' => $literaryComparison ]; } /** * 插件实现方法 * * @access public * @return void */ public static function render() { $stats = self::getStats(); $word = Options::alloc()->plugin('HelloWorld')->word; // 获取当前登录用户的信息 $user = \Typecho\Widget::widget('Widget_User'); $userName = $user->screenName ?: $user->name; // 优先使用昵称,如果没有则使用登录名 echo '<span class="helloworld-trigger" style="color:#BBBBBB">' . htmlspecialchars($word) . '</span>'; echo '<div class="helloworld-tooltip"> <h4>欢迎回来,' . htmlspecialchars($userName) . '</h4> <div style="margin-bottom:20px;"> 您已经写了 ' . $stats['postCount'] . ' 篇文章,一共 ' . $stats['totalWords'] . ' 字,' . $stats['literaryComparison'] . '。 </div> <ul> <li><strong>最近登录:</strong>' . $stats['lastLogin'] . '</li> <li><strong>最近发文:</strong>' . $stats['lastPostTime'] . '</li> </ul> </div>'; } } typecho给后台admin界面添加图表分析 https://blog.iletter.top/archives/406.html 2025-07-27T02:32:00+08:00 觉得后台admin空荡荡的,索性加个图表自己魔改一下下。以下是完整的index.php感兴趣的可以直接下载index.php文件覆盖/admin 下面的index.php文件下载地址:https://wonder1999.lanzouu.com/ikRiy32sfnah<?php include 'common.php'; include 'header.php'; include 'menu.php'; $stat = \Widget\Stat::alloc(); ?> <script src="https://blog.iletter.top/usr/blog_img/chart.js"></script> <!-- 引入 Font Awesome 图标库 --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"> <style> /* 自定义美化样式 */ .typecho-dashboard { padding: 20px 0; } .card { background-color: #fff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; } .card-title { margin-top: 0; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #eee; color: #333; font-size: 1.2em; } /* 欢迎区域 */ .welcome-card { background: linear-gradient(135deg, #1f1f20 0%, #0076bb 100%); color: white; } .welcome-card h3 { color: white; margin-top: 0; } .welcome-card p { font-size: 1.1em; margin-bottom: 20px; } .welcome-card a { color: #fff; text-decoration: underline; } .welcome-card a:hover { text-decoration: none; opacity: 0.9; } /* 快捷链接 */ #start-link { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 10px; } #start-link li { margin: 0; } #start-link a { display: inline-block; padding: 8px 16px; background-color: rgba(255, 255, 255, 0.2); border-radius: 4px; color: white !important; text-decoration: none; transition: background-color 0.3s ease; } #start-link a:hover { background-color: rgba(255, 255, 255, 0.3); } #start-link .balloon { background-color: #ff6b6b; color: white; border-radius: 10px; padding: 2px 6px; font-size: 0.8em; margin-left: 5px; } /* 统计卡片 */ .stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; } .stat-card { background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 15px; text-align: center; transition: transform 0.2s ease, box-shadow 0.2s ease; } .stat-icon { font-size: 2em; margin-bottom: 10px; color: #000000; /* 可根据不同类型调整颜色 */ } .stat-number { font-size: 1.8em; font-weight: bold; margin: 5px 0; color: #495057; } .stat-label { font-size: 0.9em; color: #6c757d; } /* 最近列表 */ .latest-link ul { list-style: none; padding: 0; margin: 0; } .latest-link li { padding: 8px 0; border-bottom: 1px solid #eee; } .latest-link li:last-child { border-bottom: none; } .latest-link .title { margin-left: 5px; } .latest-link span { color: #6c757d; font-size: 0.9em; } /* 图表区域 - 重新规划 */ .chart-container { max-width: 1200px; /* 限制图表区域最大宽度 */ margin: 0 auto; /* 水平居中 */ display: flex; flex-direction: column; gap: 20px; } .chart-row { display: flex; gap: 20px; flex-wrap: wrap; } .chart-box { flex: 1; min-width: 250px; /* 调整最小宽度以适应更多屏幕 */ background-color: #fff; border-radius: 8px; /*box-shadow: 0 2px 4px rgba(0,0,0,0.1);*/ padding: 15px; display: flex; flex-direction: column; /* 确保内部元素垂直排列 */ } .chart-box h4 { margin: 0 0 10px 0; /* 调整标题边距 */ text-align: center; color: #333; flex-shrink: 0; /* 防止标题被压缩 */ } .chart-box canvas { width: 100% !important; /* 确保画布宽度占满容器 */ max-height: 300px; /* 限制图表最大高度 */ flex-grow: 1; /* 画布占据剩余空间 */ } /* 响应式调整 */ @media (max-width: 768px) { .stat-cards { grid-template-columns: repeat(2, 1fr); } .chart-row { flex-direction: column; /* 小屏幕时堆叠 */ } .chart-box { min-width: 100%; /* 小屏幕时占满宽度 */ } #start-link { flex-direction: column; } } /* 中等屏幕调整 - 让图表在中等屏幕也能两列显示 */ @media (min-width: 769px) and (max-width: 1100px) { .chart-row { /* 在这个范围内,允许换行,但.chart-box的flex行为会使其尽可能并排 */ } .chart-box { /* 可以微调 min-width 来控制换行点 */ min-width: calc(50% - 10px); } } </style> <div class="main"> <div class="container typecho-dashboard"> <?php include 'page-title.php'; ?> <div class="row typecho-page-main"> <!-- 欢迎卡片 --> <div class="col-mb-12" role="main"> <div class="card welcome-card"> <h3><?php _e('欢迎使用 Typecho'); ?></h3> <p><?php _e('目前有 <em>%s</em> 篇文章, 并有 <em>%s</em> 条关于你的评论在 <em>%s</em> 个分类中.', $stat->myPublishedPostsNum, $stat->myPublishedCommentsNum, $stat->categoriesNum); ?></p> <ul id="start-link" class="clearfix"> <?php if ($user->pass('contributor', true)): ?> <li><a href="<?php $options->adminUrl('write-post.php'); ?>"><i class="fas fa-pen"></i> <?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'); ?>"><i class="fas fa-comments"></i> <?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'); ?>"><i class="fas fa-comments"></i> <?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'); ?>"><i class="fas fa-trash-alt"></i> <?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'); ?>"><i class="fas fa-trash-alt"></i> <?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'); ?>"><i class="fas fa-list"></i> <?php _e('文章管理'); ?></a></li> <li><a href="<?php $options->adminUrl('plugins.php'); ?>"><i class="fas fa-plug"></i> <?php _e('插件管理'); ?></a></li> <li><a href="<?php $options->adminUrl('options-general.php'); ?>"><i class="fas fa-cog"></i> <?php _e('系统设置'); ?></a></li> <?php endif; ?> <?php endif; ?> </ul> </div> </div> <!-- 统计卡片 --> <div class="col-mb-12"> <div class="stat-cards"> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-posts.php'); ?>"> <div class="stat-icon"><i class="fas fa-file-alt"></i></div> <div class="stat-number"><?php echo $stat->myPublishedPostsNum; ?></div> <div class="stat-label"><?php _e('文章'); ?></div> </a> </div> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-comments.php'); ?>"> <div class="stat-icon"><i class="fas fa-comment"></i></div> <div class="stat-number"><?php echo $stat->myPublishedCommentsNum; ?></div> <div class="stat-label"><?php _e('评论'); ?></div> </a> </div> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-categories.php'); ?>"> <div class="stat-icon"><i class="fas fa-folder"></i></div> <div class="stat-number"><?php echo $stat->categoriesNum; ?></div> <div class="stat-label"><?php _e('分类'); ?></div> </a> </div> <div class="stat-card"> <a href="<?php $options->adminUrl('manage-tags.php'); ?>"> <div class="stat-icon"><i class="fas fa-tags"></i></div> <div class="stat-number"><?php echo $stat->tagsNum; ?></div> <div class="stat-label"><?php _e('标签'); ?></div> </a> </div> <!-- 可根据需要添加更多统计 --> </div> </div> <!-- 左侧内容:最近文章 --> <div class="col-mb-12 col-tb-6" role="complementary"> <div class="card"> <h3 class="card-title"><?php _e('最近发布的文章'); ?></h3> <?php \Widget\Contents\Post\Recent::alloc('pageSize=10')->to($posts); ?> <ul class="latest-link"> <?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> </div> </div> <!-- 右侧内容:最近评论 --> <div class="col-mb-12 col-tb-6" role="complementary"> <div class="card"> <h3 class="card-title"><?php _e('最近得到的回复'); ?></h3> <ul class="latest-link"> <?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> </div> </div> <!-- 图表显示区域 --> <div class="col-mb-12" role="complementary"> <div class="card"> <h3 class="card-title" style="color: #333;"><?php _e('文章统计数据'); ?></h3> <div class="chart-container"> <div class="chart-row"> <!-- 月度发布图表 --> <div class="chart-box"> <h4><?php _e('月度文章发布趋势'); ?></h4> <canvas id="monthlyPostChart" height="250"></canvas> </div> <!-- 状态分布图表 --> <div class="chart-box"> <h4><?php _e('文章状态分布'); ?></h4> <canvas id="statusChart" height="250"></canvas> </div> </div> <div class="chart-row"> <!-- 分类文章统计 --> <div class="chart-box"> <h4><?php _e('各分类文章数量'); ?></h4> <canvas id="categoryChart" height="250"></canvas> </div> <!-- 标签文章统计 --> <div class="chart-box"> <h4><?php _e('热门标签文章数量 (Top10)'); ?></h4> <canvas id="tagChart" height="250"></canvas> </div> </div> </div> </div> </div> </div> </div> </div> <?php include 'copyright.php'; include 'common-js.php'; ?> <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') // 注意:Typecho 草稿通常在 contents 表中 type 为 post_draft // ->where('status = ?', 'publish') // 草稿状态通常不是 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(54, 162, 235)', // 调整为蓝色系 backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, // 稍微增加曲线张力 fill: true }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: 'rgba(0, 0, 0, 0.05)' // 网格线颜色 } }, x: { grid: { color: 'rgba(0, 0, 0, 0.05)' } } }, plugins: { legend: { display: false // 隐藏图例,因为只有一个数据集 }, tooltip: { mode: 'index', intersect: false } } } }); } // 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, borderColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 15, usePointStyle: true } }, tooltip: { callbacks: { label: function(context) { return context.label + ': ' + context.raw; } } } }, cutout: '60%' // 增加中间空心部分 } }); } // 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.7)', // 紫色系 borderColor: 'rgba(153, 102, 255, 1)', borderWidth: 1, borderRadius: 4, // 条形圆角 barPercentage: 0.7, // 调整条形宽度 categoryPercentage: 0.8 }] }, options: { indexAxis: 'x', // 保持为垂直柱状图 responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: 'rgba(0, 0, 0, 0.05)' } }, x: { grid: { display: false // 隐藏 X 轴网格线 } } }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } } } }); } // 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.7)', // 橙色系 borderColor: 'rgba(255, 159, 64, 1)', borderWidth: 1, borderRadius: 4, barPercentage: 0.7, categoryPercentage: 0.8 }] }, options: { indexAxis: 'x', responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 }, grid: { color: 'rgba(0, 0, 0, 0.05)' } }, x: { grid: { display: false } } }, plugins: { legend: { display: false }, tooltip: { mode: 'index', intersect: false } } } }); } }); </script> <script> $(document).ready(function () { }); </script> <?php include 'footer.php'; ?> 其他插件推荐:[post cid="405" /] 为typecho博客添加Gotify插件通知 https://blog.iletter.top/archives/405.html 2025-07-27T02:05:00+08:00 受限于typecho博客没有通知,自己写了一个博客有评论就通知的gotify插件,脚本使用之前的python改的。这是效果下载地址插件+app+docker部署https://wonder1999.lanzouu.com/iB6DK32mdwcf代码 <?php namespace TypechoPlugin\GotifyNotify; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Typecho\Widget\Helper\Form\Element\Text; 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定义的Priority)')); $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'); // 检查必要配置 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); } } }