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);
}
}
}