From 4db044fa61391ee733f60fac1f3c7a580c5b3388 Mon Sep 17 00:00:00 2001 From: XIGE <710062962@qq.com> Date: Mon, 23 Feb 2026 21:11:30 +0800 Subject: [PATCH] 1.0 --- Panel.php | 1430 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Plugin.php | 155 ++++++ 2 files changed, 1585 insertions(+) create mode 100644 Panel.php create mode 100644 Plugin.php diff --git a/Panel.php b/Panel.php new file mode 100644 index 0000000..4768936 --- /dev/null +++ b/Panel.php @@ -0,0 +1,1430 @@ +pass('administrator', true)) { + die('无权限访问'); +} + +$options = \Widget\Options::alloc(); + +// 从数据库读取路由设置 +$db = \Typecho\Db::get(); +$settingQuery = $db->select()->from('table.options')->where('name = ?', 'routingTable'); +$settingRow = $db->fetchRow($settingQuery); +$routingTable = @unserialize($settingRow['value']); +// post 路由的 key 可能是 'post' 或 'archives' +$postUrlRule = null; +if (isset($routingTable[0]['post']['url'])) { + $postUrlRule = $routingTable[0]['post']['url']; +} elseif (isset($routingTable['post']['url'])) { + $postUrlRule = $routingTable['post']['url']; +} elseif (isset($routingTable[0]['archives']['url'])) { + $postUrlRule = $routingTable[0]['archives']['url']; +} elseif (isset($routingTable['archives']['url'])) { + $postUrlRule = $routingTable['archives']['url']; +} +if (empty($postUrlRule)) { + $postUrlRule = '/archives/{cid}/'; +} +// 检测是否包含 .html 后缀(兼容 [cid:digital] 和 {cid} 两种格式) +if (strpos($postUrlRule, '.html') !== false) { + $postUrlFormat = $options->siteUrl . '%s.html'; +} else { + $postUrlFormat = $options->siteUrl . '%s/'; +} + +// 标签地址格式 - 修正为 tag-slug.html +$tagUrlFormat = $options->siteUrl . 'tag-%s.html'; + +try { + $pluginConfig = $options->plugin('YearlyData'); + $topLimit = isset($pluginConfig->topLimit) ? intval($pluginConfig->topLimit) : 10; + $includeDraft = isset($pluginConfig->includeDraft) ? $pluginConfig->includeDraft : '0'; + $chartColor = isset($pluginConfig->chartColor) ? $pluginConfig->chartColor : '#667eea'; + + // 从数据库读取当前年份的目标设置 + // 使用独立的配置存储,键名为:yearly_target_2026 + $currentYear = isset($_GET['year']) ? intval($_GET['year']) : intval(date('Y')); + + $targetsQuery = $db->select('value')->from('table.options')->where('name = ?', 'yearly_target_' . $currentYear); + $targetsRow = $db->fetchRow($targetsQuery); + + if ($targetsRow && $targetsRow['value']) { + $targets = @unserialize($targetsRow['value']); + $targetPosts = isset($targets['posts']) ? intval($targets['posts']) : 0; + $targetComments = isset($targets['comments']) ? intval($targets['comments']) : 0; + $targetImages = isset($targets['images']) ? intval($targets['images']) : 0; + $targetWords = isset($targets['words']) ? intval($targets['words']) : 0; + } else { + // 如果没有找到该年份的目标设置,检查是否是当前年份且插件配置中有设置 + if ($currentYear == date('Y')) { + $targetPosts = isset($pluginConfig->targetPosts) ? intval($pluginConfig->targetPosts) : 0; + $targetComments = isset($pluginConfig->targetComments) ? intval($pluginConfig->targetComments) : 0; + $targetImages = isset($pluginConfig->targetImages) ? intval($pluginConfig->targetImages) : 0; + $targetWords = isset($pluginConfig->targetWords) ? intval($pluginConfig->targetWords) : 0; + + // 如果是当前年份且有设置目标,保存到数据库 + if ($targetPosts > 0 || $targetComments > 0 || $targetImages > 0 || $targetWords > 0) { + $targetsData = serialize([ + 'posts' => $targetPosts, + 'comments' => $targetComments, + 'images' => $targetImages, + 'words' => $targetWords + ]); + + // 检查是否已存在 + $checkQuery = $db->select('value')->from('table.options')->where('name = ?', 'yearly_target_' . $currentYear); + $checkRow = $db->fetchRow($checkQuery); + + if ($checkRow) { + // 更新 + $db->query($db->update('table.options')->rows(['value' => $targetsData])->where('name = ?', 'yearly_target_' . $currentYear)); + } else { + // 插入 + $db->query($db->insert('table.options')->rows([ + 'name' => 'yearly_target_' . $currentYear, + 'user' => 0, + 'value' => $targetsData + ])); + } + } + } else { + $targetPosts = 0; + $targetComments = 0; + $targetImages = 0; + $targetWords = 0; + } + } +} catch (Exception $e) { + $topLimit = 10; + $includeDraft = '0'; + $chartColor = '#667eea'; + $targetPosts = 0; + $targetComments = 0; + $targetImages = 0; + $targetWords = 0; +} + +$currentYear = isset($_GET['year']) ? intval($_GET['year']) : intval(date('Y')); + +// 获取可用年份 +$yearsQuery = $db->select('DISTINCT FROM_UNIXTIME(created, "%Y") as year') + ->from('table.contents') + ->where('type = ?', 'post') + ->order('year', \Typecho\Db::SORT_DESC); +$yearsRows = $db->fetchAll($yearsQuery); +$availableYears = []; +foreach ($yearsRows as $row) { + if (!empty($row['year'])) { + $availableYears[] = $row['year']; + } +} +if (empty($availableYears)) { + $availableYears = [date('Y')]; +} + +// 时间范围 +$startTime = mktime(0, 0, 0, 1, 1, $currentYear); +$endTime = mktime(23, 59, 59, 12, 31, $currentYear); +$statusCondition = ($includeDraft === '1') ? "status IN ('publish', 'draft')" : "status = 'publish'"; + +// 文章总数 +$totalPostsQuery = $db->select('COUNT(*) as total') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition); +$totalPosts = intval($db->fetchRow($totalPostsQuery)['total']); + +// 评论总数 +$commentsQuery = $db->select('COUNT(*) as total') + ->from('table.comments') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where('status = ?', 'approved'); +$totalComments = intval($db->fetchRow($commentsQuery)['total']); + +// 图片总数(新增) +$imagesQuery = $db->select('COUNT(*) as total') + ->from('table.contents') + ->where('type = ?', 'attachment') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where('text LIKE ?', '%.jpg%'); +$imagesResult = $db->fetchRow($imagesQuery); +$totalImages = intval($imagesResult['total']); + +// 总字数(同时获取 cid, title, slug 供后续最长/最短文章使用) +$textsQuery = $db->select('cid, title, slug, text') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition); +$textsRows = $db->fetchAll($textsQuery); +$totalWords = 0; +foreach ($textsRows as $row) { + $text = strip_tags($row['text']); + $text = preg_replace('/\s+/', '', $text); + $totalWords += mb_strlen($text, 'UTF-8'); +} +$averageWords = $totalPosts > 0 ? intval($totalWords / $totalPosts) : 0; +$averageComments = $totalPosts > 0 ? round($totalComments / $totalPosts, 2) : 0; + +// 计算目标完成百分比 +function calculatePercentage($current, $target) { + if ($target <= 0) return 0; + return min(100, round(($current / $target) * 100, 1)); +} + +$postsPercentage = calculatePercentage($totalPosts, $targetPosts); +$commentsPercentage = calculatePercentage($totalComments, $targetComments); +$imagesPercentage = calculatePercentage($totalImages, $targetImages); +$wordsPercentage = calculatePercentage($totalWords, $targetWords); + +// 浏览量 +$totalViews = 0; +try { + $viewsQuery = $db->select('SUM(views) as total') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition); + $viewsResult = $db->fetchRow($viewsQuery); + $totalViews = intval($viewsResult['total']); +} catch (Exception $e) {} + +// 按月统计文章 +$monthlyQuery = $db->select('FROM_UNIXTIME(created, "%m") as month, COUNT(*) as count') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition) + ->group('month') + ->order('month', \Typecho\Db::SORT_ASC); +$monthlyRows = $db->fetchAll($monthlyQuery); +$monthlyData = []; +for ($i = 1; $i <= 12; $i++) { + $monthlyData[str_pad($i, 2, '0', STR_PAD_LEFT)] = 0; +} +foreach ($monthlyRows as $row) { + $monthlyData[$row['month']] = intval($row['count']); +} + +// 按月统计图片(新增) +$monthlyImagesQuery = $db->select('FROM_UNIXTIME(created, "%m") as month, COUNT(*) as count') + ->from('table.contents') + ->where('type = ?', 'attachment') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where('text LIKE ?', '%.jpg%') + ->group('month') + ->order('month', \Typecho\Db::SORT_ASC); +$monthlyImagesRows = $db->fetchAll($monthlyImagesQuery); +$monthlyImagesData = []; +for ($i = 1; $i <= 12; $i++) { + $monthlyImagesData[str_pad($i, 2, '0', STR_PAD_LEFT)] = 0; +} +foreach ($monthlyImagesRows as $row) { + $monthlyImagesData[$row['month']] = intval($row['count']); +} + +// 按时段统计 +$hourlyQuery = $db->select('FROM_UNIXTIME(created, "%H") as hour, COUNT(*) as count') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition) + ->group('hour'); +$hourlyRows = $db->fetchAll($hourlyQuery); +$hourlyData = ['凌晨 (0-6点)' => 0, '上午 (6-12点)' => 0, '下午 (12-18点)' => 0, '晚上 (18-24点)' => 0]; +foreach ($hourlyRows as $row) { + $hour = intval($row['hour']); + $count = intval($row['count']); + if ($hour < 6) $hourlyData['凌晨 (0-6点)'] += $count; + elseif ($hour < 12) $hourlyData['上午 (6-12点)'] += $count; + elseif ($hour < 18) $hourlyData['下午 (12-18点)'] += $count; + else $hourlyData['晚上 (18-24点)'] += $count; +} + +// 评论排行 +$commentRankQuery = $db->select('cid, title, commentsNum') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition) + ->order('commentsNum', \Typecho\Db::SORT_DESC) + ->limit($topLimit); +$topByComments = $db->fetchAll($commentRankQuery); + +// 浏览排行 +$topByViews = []; +try { + $viewRankQuery = $db->select('cid, title, views') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition) + ->order('views', \Typecho\Db::SORT_DESC) + ->limit($topLimit); + $topByViews = $db->fetchAll($viewRankQuery); +} catch (Exception $e) {} + +// 活跃评论者(同时获取评论者网址)- 修改这里:添加排除管理员的条件 +$commenterQuery = $db->select('author, mail, url, COUNT(*) as count') + ->from('table.comments') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where('status = ?', 'approved') + ->where('authorId != ?', $user->uid) // 排除当前登录的管理员 + ->group('mail') + ->order('count', \Typecho\Db::SORT_DESC) + ->limit($topLimit); +$topCommenters = $db->fetchAll($commenterQuery); + +// 获取最近四年的数据(按年份升序排列,从左到右:最老 -> 最新) +$fourYearData = []; +$yearsToShow = []; +for ($i = 3; $i >= 0; $i--) { + $year = $currentYear - $i; + if (in_array($year, $availableYears) || $i == 0) { + $yearsToShow[] = $year; + + $yearStart = mktime(0, 0, 0, 1, 1, $year); + $yearEnd = mktime(23, 59, 59, 12, 31, $year); + + // 文章数 + $postsQuery = $db->select('COUNT(*) as total')->from('table.contents') + ->where('type = ?', 'post')->where('created >= ?', $yearStart) + ->where('created <= ?', $yearEnd)->where($statusCondition); + $posts = intval($db->fetchRow($postsQuery)['total']); + + // 评论数 + $commsQuery = $db->select('COUNT(*) as total')->from('table.comments') + ->where('created >= ?', $yearStart)->where('created <= ?', $yearEnd) + ->where('status = ?', 'approved'); + $comments = intval($db->fetchRow($commsQuery)['total']); + + // 图片附件数(新增) + $imagesQuery = $db->select('COUNT(*) as total')->from('table.contents') + ->where('type = ?', 'attachment') + ->where('created >= ?', $yearStart) + ->where('created <= ?', $yearEnd) + ->where('text LIKE ?', '%.jpg%'); + $imagesResult = $db->fetchRow($imagesQuery); + $images = intval($imagesResult['total']); + + // 总字数 + $wordsQuery = $db->select('text')->from('table.contents') + ->where('type = ?', 'post')->where('created >= ?', $yearStart) + ->where('created <= ?', $yearEnd)->where($statusCondition); + $wordsRows = $db->fetchAll($wordsQuery); + $words = 0; + foreach ($wordsRows as $row) { + $text = strip_tags($row['text']); + $text = preg_replace('/\s+/', '', $text); + $words += mb_strlen($text, 'UTF-8'); + } + + $fourYearData[$year] = [ + 'posts' => $posts, + 'comments' => $comments, + 'images' => $images, // 新增图片数 + 'words' => $words + ]; + } +} + +// 如果不够4年,补全显示最近4年 +if (count($yearsToShow) < 4) { + $yearsToShow = []; + for ($i = 3; $i >= 0; $i--) { + $year = $currentYear - $i; + if (!in_array($year, $yearsToShow)) { + $yearsToShow[] = $year; + } + } +} + +// 确保数组包含所有年份的数据 +foreach ($yearsToShow as $year) { + if (!isset($fourYearData[$year])) { + $fourYearData[$year] = [ + 'posts' => 0, + 'comments' => 0, + 'images' => 0, // 新增图片数 + 'words' => 0 + ]; + } +} + +// 排序确保显示顺序正确 +sort($yearsToShow); + +// 按周统计 +$weeklyQuery = $db->select('WEEK(FROM_UNIXTIME(created), 1) as week, COUNT(*) as count') + ->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition) + ->group('week') + ->order('week', \Typecho\Db::SORT_ASC); +$weeklyRows = $db->fetchAll($weeklyQuery); +$weeklyData = []; +foreach ($weeklyRows as $row) { + $weeklyData['第' . intval($row['week']) . '周'] = intval($row['count']); +} + +// 最长文章 +$longestPost = null; +$maxWords = 0; +foreach ($textsRows as $row) { + $text = strip_tags($row['text']); + $text = preg_replace('/\s+/', '', $text); + $words = mb_strlen($text, 'UTF-8'); + if ($words > $maxWords) { + $maxWords = $words; + $longestPost = [ + 'cid' => $row['cid'], + 'title' => $row['title'], + 'slug' => $row['slug'], + 'words' => $words + ]; + } +} + +// 最短文章 +$shortestPost = null; +$minWords = PHP_INT_MAX; +foreach ($textsRows as $row) { + $text = strip_tags($row['text']); + $text = preg_replace('/\s+/', '', $text); + $words = mb_strlen($text, 'UTF-8'); + if ($words > 0 && $words < $minWords) { + $minWords = $words; + $shortestPost = [ + 'cid' => $row['cid'], + 'title' => $row['title'], + 'slug' => $row['slug'], + 'words' => $words + ]; + } +} + +// 先获取符合条件的文章CID列表 +$cidsQuery = $db->select('cid')->from('table.contents') + ->where('type = ?', 'post') + ->where('created >= ?', $startTime) + ->where('created <= ?', $endTime) + ->where($statusCondition); +$cidsRows = $db->fetchAll($cidsQuery); +$cids = array_column($cidsRows, 'cid'); +if (empty($cids)) { + $cids = [0]; +} + +// 获取标签分布(用两步查询避免JOIN问题) +// 第一步:获取本年度文章的所有标签mid +$midQuery = $db->select(' DISTINCT mid')->from('table.relationships') + ->where('cid IN ?', $cids); +$midRows = $db->fetchAll($midQuery); +$mids = array_column($midRows, 'mid'); +if (empty($mids)) { + $tagDistribution = []; +} else { + // 第二步:获取每个标签的文章数,只保留type=tag的 + $tagDistribution = []; + foreach ($mids as $mid) { + $count = $db->fetchRow($db->select('COUNT(*) as count')->from('table.relationships') + ->where('mid = ?', $mid)->where('cid IN ?', $cids)); + $meta = $db->fetchRow($db->select('name', 'slug')->from('table.metas')->where('mid = ?', $mid)->where('type = ?', 'tag')); + if ($meta && $count['count'] > 0) { + $tagDistribution[] = [ + 'name' => $meta['name'], + 'slug' => $meta['slug'], + 'count' => intval($count['count']) + ]; + } + } + // 按数量排序 + usort($tagDistribution, function($a, $b) { return $b['count'] - $a['count']; }); + $tagDistribution = array_slice($tagDistribution, 0, $topLimit); +} + +// 分类分布(用两步查询避免JOIN问题) +// 第一步:获取本年度文章的所有分类mid +$catMidQuery = $db->select(' DISTINCT mid')->from('table.relationships') + ->where('cid IN ?', $cids); +$catMidRows = $db->fetchAll($catMidQuery); +$catMids = array_column($catMidRows, 'mid'); +if (empty($catMids)) { + $categoryDistribution = []; +} else { + // 第二步:获取每个分类的文章数,只保留type=category的 + $categoryDistribution = []; + foreach ($catMids as $mid) { + $count = $db->fetchRow($db->select('COUNT(*) as count')->from('table.relationships') + ->where('mid = ?', $mid)->where('cid IN ?', $cids)); + $meta = $db->fetchRow($db->select('name', 'slug')->from('table.metas')->where('mid = ?', $mid)->where('type = ?', 'category')); + if ($meta && $count['count'] > 0) { + $categoryDistribution[] = [ + 'name' => $meta['name'], + 'slug' => $meta['slug'], + 'count' => intval($count['count']) + ]; + } + } + // 按数量排序 + usort($categoryDistribution, function($a, $b) { return $b['count'] - $a['count']; }); +} +?> + + + +