1430 lines
47 KiB
PHP
1430 lines
47 KiB
PHP
<?php
|
||
include 'common.php';
|
||
include 'header.php';
|
||
include 'menu.php';
|
||
|
||
// 添加 Helper 类的使用声明
|
||
use Utils\Helper;
|
||
|
||
$user = \Typecho\Widget::widget('Widget_User');
|
||
if (!$user->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']; });
|
||
}
|
||
?>
|
||
|
||
<style>
|
||
.ys-toolbar {
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding: 15px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
}
|
||
.ys-toolbar-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-left: 0;
|
||
}
|
||
.ys-toolbar-left label {
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
.ys-toolbar select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
width: 120px;
|
||
height: auto !important;
|
||
line-height: normal !important;
|
||
min-height: 36px;
|
||
}
|
||
.ys-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
.ys-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
.ys-card-icon {
|
||
width: 45px;
|
||
height: 45px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
margin-right: 12px;
|
||
color: #fff;
|
||
}
|
||
.ys-card-value {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: #333;
|
||
}
|
||
.ys-card-label {
|
||
font-size: 12px;
|
||
color: #888;
|
||
margin-top: 2px;
|
||
}
|
||
.ys-section {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||
}
|
||
.ys-section .typecho-list-table th{
|
||
color:#000!important;
|
||
}
|
||
.ys-section .typecho-list-table td{
|
||
color:#444!important;
|
||
}
|
||
.ys-section .typecho-list-table tr:hover{
|
||
background-color: #FFF!important;
|
||
color: #FFF!important;
|
||
}
|
||
.ys-section .typecho-list-table td:hover{
|
||
background-color: #FFF!important;
|
||
color: #FFF;
|
||
}
|
||
.ys-section-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #eee;
|
||
color:#000!important;
|
||
}
|
||
.ys-chart-container {
|
||
position: relative;
|
||
height: 280px;
|
||
}
|
||
.ys-chart-small {
|
||
height: 220px;
|
||
}
|
||
.ys-row {
|
||
display: flex;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.ys-col-6 {
|
||
flex: 1;
|
||
min-width: 300px;
|
||
}
|
||
.ys-col-6 .ys-section {
|
||
margin-bottom: 0;
|
||
}
|
||
.ys-growth-up {
|
||
color: #52c41a;
|
||
font-weight: 600;
|
||
}
|
||
.ys-growth-down {
|
||
color: #ff4d4f;
|
||
font-weight: 600;
|
||
}
|
||
.ys-empty {
|
||
padding: 30px;
|
||
text-align: center;
|
||
color: #999;
|
||
}
|
||
.ys-footer {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
.ys-footer a {
|
||
color: #667eea;
|
||
text-decoration: none;
|
||
}
|
||
.ys-link {
|
||
color: #667eea;
|
||
text-decoration: none;
|
||
}
|
||
.ys-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* 评论和评论者容器统一样式 */
|
||
.ys-comments-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-height: 300px;
|
||
}
|
||
.ys-comment-item,
|
||
.ys-commenter-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px 15px;
|
||
background: #fff;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 6px;
|
||
margin: 0;
|
||
height: 48px;
|
||
box-sizing: border-box;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.ys-comment-item:hover,
|
||
.ys-commenter-item:hover {
|
||
border-color: #667eea;
|
||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
|
||
}
|
||
.ys-comment-rank {
|
||
width: 30px;
|
||
text-align: center;
|
||
font-weight: bold;
|
||
color: #667eea;
|
||
flex-shrink: 0;
|
||
}
|
||
.ys-comment-content {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-width: 0;
|
||
height: 100%;
|
||
}
|
||
.ys-comment-title {
|
||
font-size: 14px;
|
||
color: #333;
|
||
flex: 1;
|
||
margin-right: 15px;
|
||
min-width: 0;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.ys-comment-title a {
|
||
color: #333;
|
||
text-decoration: none;
|
||
}
|
||
.ys-comment-title a:hover {
|
||
color: #667eea;
|
||
text-decoration: underline;
|
||
}
|
||
.ys-commenter-name {
|
||
font-size: 14px;
|
||
color: #333;
|
||
flex: 1;
|
||
min-width: 0;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.ys-commenter-name a {
|
||
color: #333;
|
||
text-decoration: none;
|
||
}
|
||
.ys-commenter-name a:hover {
|
||
color: #667eea;
|
||
text-decoration: underline;
|
||
}
|
||
.ys-comment-count {
|
||
font-weight: bold;
|
||
color: #667eea;
|
||
flex-shrink: 0;
|
||
padding-left: 10px;
|
||
}
|
||
.ys-title-truncate {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
max-width: 100%;
|
||
display: inline-block;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
/* 标签样式 */
|
||
.ys-tag-item {
|
||
display: inline-block;
|
||
padding: 5px 12px;
|
||
margin: 0 8px 8px 0;
|
||
background: #f0f0f0;
|
||
border-radius: 15px;
|
||
font-size: 13px;
|
||
color: #333;
|
||
text-decoration: none;
|
||
transition: all 0.2s ease;
|
||
}
|
||
.ys-tag-item:hover {
|
||
background: #667eea;
|
||
color: #fff;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
/* 年度对比模块样式 */
|
||
.ys-year-compare {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.ys-year-header {
|
||
display: flex;
|
||
background: #f8f9fa;
|
||
border-radius: 6px 6px 0 0;
|
||
border: 1px solid #e8e8e8;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.ys-year-header-cell {
|
||
flex: 1;
|
||
padding: 12px 15px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
color: #333;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.ys-year-header-cell:first-child {
|
||
flex: 0.6;
|
||
min-width: 100px;
|
||
text-align: left;
|
||
background: #f1f3f5;
|
||
border-right: 1px solid #e8e8e8;
|
||
}
|
||
|
||
.ys-year-row {
|
||
display: flex;
|
||
border-left: 1px solid #e8e8e8;
|
||
border-right: 1px solid #e8e8e8;
|
||
border-bottom: 1px solid #e8e8e8;
|
||
}
|
||
|
||
.ys-year-row:last-child {
|
||
border-radius: 0 0 6px 6px;
|
||
}
|
||
|
||
.ys-year-row-cell {
|
||
flex: 1;
|
||
padding: 12px 15px;
|
||
text-align: center;
|
||
color: #555;
|
||
min-width: 120px;
|
||
border-right: 1px solid #e8e8e8;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.ys-year-row-cell:first-child {
|
||
flex: 0.6;
|
||
min-width: 100px;
|
||
text-align: left;
|
||
justify-content: flex-start;
|
||
background: #f8f9fa;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.ys-year-row-cell:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.ys-year-value {
|
||
font-weight: 600;
|
||
color: #000;
|
||
}
|
||
|
||
/* 年度目标进度条样式(新增) */
|
||
.ys-target-container {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.ys-target-item {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
border: 1px solid #f0f0f0;
|
||
}
|
||
|
||
.ys-target-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.ys-target-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.ys-target-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
color: #fff;
|
||
}
|
||
|
||
.ys-target-count {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #667eea;
|
||
}
|
||
|
||
.ys-target-progress {
|
||
height: 10px;
|
||
background: #f0f0f0;
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.ys-target-progress-bar {
|
||
height: 100%;
|
||
border-radius: 5px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.ys-target-percentage {
|
||
font-size: 12px;
|
||
color: #888;
|
||
text-align: center;
|
||
}
|
||
|
||
.ys-target-completed {
|
||
color: #52c41a;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.ys-target-incomplete {
|
||
color: #ff4d4f;
|
||
font-weight: 600;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.ys-row {
|
||
flex-direction: column;
|
||
}
|
||
.ys-cards {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
.ys-title-truncate {
|
||
max-width: 200px;
|
||
}
|
||
.ys-toolbar-left {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.ys-year-header-cell,
|
||
.ys-year-row-cell {
|
||
min-width: 90px;
|
||
}
|
||
|
||
.ys-year-header-cell:first-child,
|
||
.ys-year-row-cell:first-child {
|
||
min-width: 80px;
|
||
}
|
||
|
||
.ys-target-container {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<div class="main">
|
||
<div class="body container">
|
||
<div class="ys-toolbar" style="margin-left:0px;text-align:center;justify-content:center;">
|
||
<div class="ys-toolbar-left">
|
||
<label></label>
|
||
<select id="year-select" onchange="location.href='<?php echo $options->adminUrl; ?>extending.php?panel=YearlyData%2FPanel.php&year='+this.value">
|
||
<?php foreach ($availableYears as $year): ?>
|
||
<option value="<?php echo $year; ?>" <?php echo $year == $currentYear ? 'selected' : ''; ?>><?php echo $year; ?>年</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 年度目标进度条模块 -->
|
||
<?php if ($targetPosts > 0 || $targetComments > 0 || $targetImages > 0 || $targetWords > 0): ?>
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title"><!--<?php echo $currentYear; ?>-->年度目标</h3>
|
||
<div class="ys-target-container">
|
||
<?php if ($targetPosts > 0): ?>
|
||
<div class="ys-target-item">
|
||
<div class="ys-target-header">
|
||
<div class="ys-target-title">
|
||
|
||
文章数
|
||
</div>
|
||
<div class="ys-target-count"><?php echo number_format($totalPosts); ?> / <?php echo number_format($targetPosts); ?></div>
|
||
</div>
|
||
<div class="ys-target-progress">
|
||
<div class="ys-target-progress-bar" style="width: <?php echo $postsPercentage; ?>%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);"></div>
|
||
</div>
|
||
<div class="ys-target-percentage <?php echo $postsPercentage >= 100 ? 'ys-target-completed' : 'ys-target-incomplete'; ?>">
|
||
<?php echo $postsPercentage; ?>%
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($targetComments > 0): ?>
|
||
<div class="ys-target-item">
|
||
<div class="ys-target-header">
|
||
<div class="ys-target-title">
|
||
|
||
评论数
|
||
</div>
|
||
<div class="ys-target-count"><?php echo number_format($totalComments); ?> / <?php echo number_format($targetComments); ?></div>
|
||
</div>
|
||
<div class="ys-target-progress">
|
||
<div class="ys-target-progress-bar" style="width: <?php echo $commentsPercentage; ?>%; background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);"></div>
|
||
</div>
|
||
<div class="ys-target-percentage <?php echo $commentsPercentage >= 100 ? 'ys-target-completed' : 'ys-target-incomplete'; ?>">
|
||
<?php echo $commentsPercentage; ?>%
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($targetImages > 0): ?>
|
||
<div class="ys-target-item">
|
||
<div class="ys-target-header">
|
||
<div class="ys-target-title">
|
||
|
||
图片数
|
||
</div>
|
||
<div class="ys-target-count"><?php echo number_format($totalImages); ?> / <?php echo number_format($targetImages); ?></div>
|
||
</div>
|
||
<div class="ys-target-progress">
|
||
<div class="ys-target-progress-bar" style="width: <?php echo $imagesPercentage; ?>%; background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);"></div>
|
||
</div>
|
||
<div class="ys-target-percentage <?php echo $imagesPercentage >= 100 ? 'ys-target-completed' : 'ys-target-incomplete'; ?>">
|
||
<?php echo $imagesPercentage; ?>%
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($targetWords > 0): ?>
|
||
<div class="ys-target-item">
|
||
<div class="ys-target-header">
|
||
<div class="ys-target-title">
|
||
|
||
总字数
|
||
</div>
|
||
<div class="ys-target-count"><?php echo number_format($totalWords); ?> / <?php echo number_format($targetWords); ?></div>
|
||
</div>
|
||
<div class="ys-target-progress">
|
||
<div class="ys-target-progress-bar" style="width: <?php echo $wordsPercentage; ?>%; background: linear-gradient(90deg, #43e97b 0%, #38f9d7 100%);"></div>
|
||
</div>
|
||
<div class="ys-target-percentage <?php echo $wordsPercentage >= 100 ? 'ys-target-completed' : 'ys-target-incomplete'; ?>">
|
||
<?php echo $wordsPercentage; ?>%
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">年度概览</h3>
|
||
<div class="ys-cards">
|
||
<div class="ys-card">
|
||
|
||
<div><div class="ys-card-value"><?php echo number_format($totalPosts); ?></div><div class="ys-card-label">总文章</div></div>
|
||
</div>
|
||
<div class="ys-card">
|
||
|
||
<div><div class="ys-card-value"><?php echo number_format($totalWords); ?></div><div class="ys-card-label">总字数</div></div>
|
||
</div>
|
||
<div class="ys-card">
|
||
|
||
<div><div class="ys-card-value"><?php echo number_format($totalComments); ?></div><div class="ys-card-label">总评论</div></div>
|
||
</div>
|
||
<div class="ys-card">
|
||
|
||
<div><div class="ys-card-value"><?php echo number_format($averageWords); ?></div><div class="ys-card-label">平均字数</div></div>
|
||
</div>
|
||
<div class="ys-card">
|
||
|
||
<div><div class="ys-card-value"><?php echo $averageComments; ?></div><div class="ys-card-label">平均评论</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">年度对比</h3>
|
||
<div class="ys-year-compare">
|
||
<div class="ys-year-header">
|
||
<div class="ys-year-header-cell">统计年份</div>
|
||
<?php foreach ($yearsToShow as $year): ?>
|
||
<div class="ys-year-header-cell"><?php echo $year; ?>年</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="ys-year-row">
|
||
<div class="ys-year-row-cell">文章数</div>
|
||
<?php foreach ($yearsToShow as $year): ?>
|
||
<div class="ys-year-row-cell">
|
||
<span class="ys-year-value">
|
||
<?php echo isset($fourYearData[$year]) ? number_format($fourYearData[$year]['posts']) : '0'; ?>
|
||
</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="ys-year-row">
|
||
<div class="ys-year-row-cell">评论数</div>
|
||
<?php foreach ($yearsToShow as $year): ?>
|
||
<div class="ys-year-row-cell">
|
||
<span class="ys-year-value">
|
||
<?php echo isset($fourYearData[$year]) ? number_format($fourYearData[$year]['comments']) : '0'; ?>
|
||
</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="ys-year-row">
|
||
<div class="ys-year-row-cell">图片数</div>
|
||
<?php foreach ($yearsToShow as $year): ?>
|
||
<div class="ys-year-row-cell">
|
||
<span class="ys-year-value">
|
||
<?php echo isset($fourYearData[$year]) ? number_format($fourYearData[$year]['images']) : '0'; ?>
|
||
</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="ys-year-row">
|
||
<div class="ys-year-row-cell">总字数</div>
|
||
<?php foreach ($yearsToShow as $year): ?>
|
||
<div class="ys-year-row-cell">
|
||
<span class="ys-year-value">
|
||
<?php echo isset($fourYearData[$year]) ? number_format($fourYearData[$year]['words']) : '0'; ?>
|
||
</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">月度数据(文章)</h3>
|
||
<div class="ys-chart-container"><canvas id="monthlyChart"></canvas></div>
|
||
</div>
|
||
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">月度数据(图片)</h3>
|
||
<div class="ys-chart-container"><canvas id="monthlyImagesChart"></canvas></div>
|
||
</div>
|
||
|
||
<div class="ys-row">
|
||
<div class="ys-col-6">
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">时段数据</h3>
|
||
<div class="ys-chart-container ys-chart-small"><canvas id="hourlyChart"></canvas></div>
|
||
</div>
|
||
</div>
|
||
<div class="ys-col-6">
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">分类数据</h3>
|
||
<?php if (!empty($categoryDistribution)): ?>
|
||
<div class="ys-chart-container ys-chart-small"><canvas id="categoryChart"></canvas></div>
|
||
<?php else: ?><div class="ys-empty">暂无分类</div><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ys-row">
|
||
<div class="ys-col-6">
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">标签数据</h3>
|
||
<?php if (!empty($tagDistribution)): ?>
|
||
<div style="display: flex; flex-wrap: wrap;">
|
||
<?php foreach ($tagDistribution as $tag): ?>
|
||
<a href="<?php echo sprintf($tagUrlFormat, $tag['slug']); ?>" class="ys-tag-item" target="_blank">
|
||
<?php echo htmlspecialchars($tag['name']); ?> (<?php echo $tag['count']; ?>)
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php else: ?><div class="ys-empty">暂无标签</div><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ys-row">
|
||
<div class="ys-col-6">
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">文章评论</h3>
|
||
<?php if (!empty($topByComments)): ?>
|
||
<div class="ys-comments-container">
|
||
<?php foreach ($topByComments as $i => $post):
|
||
// 使用 Typecho 内置方法生成文章链接
|
||
$postUrl = Helper::widgetById('contents', $post['cid'])->permalink;
|
||
?>
|
||
<div class="ys-comment-item">
|
||
<div class="ys-comment-rank"><?php echo $i + 1; ?></div>
|
||
<div class="ys-comment-content">
|
||
<div class="ys-comment-title">
|
||
<span class="ys-title-truncate">
|
||
<a href="<?php echo $postUrl; ?>" class="ys-link" target="_blank"><?php echo htmlspecialchars($post['title']); ?></a>
|
||
</span>
|
||
</div>
|
||
<div class="ys-comment-count"><?php echo number_format($post['commentsNum']); ?></div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php else: ?><div class="ys-empty">暂无数据</div><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="ys-col-6">
|
||
<div class="ys-section">
|
||
<h3 class="ys-section-title">活跃用户</h3>
|
||
<?php if (!empty($topCommenters)): ?>
|
||
<div class="ys-comments-container">
|
||
<?php foreach ($topCommenters as $i => $c): ?>
|
||
<div class="ys-commenter-item">
|
||
<div class="ys-comment-rank"><?php echo $i + 1; ?></div>
|
||
<div class="ys-comment-content">
|
||
<div class="ys-commenter-name">
|
||
<?php if (!empty($c['url'])): ?>
|
||
<a href="<?php echo htmlspecialchars($c['url']); ?>" class="ys-link" target="_blank"><?php echo htmlspecialchars($c['author']); ?></a>
|
||
<?php else: ?>
|
||
<?php echo htmlspecialchars($c['author']); ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="ys-comment-count"><?php echo $c['count']; ?></div>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php else: ?><div class="ys-empty">暂无数据</div><?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
<script>
|
||
const chartColor = '<?php echo $chartColor; ?>';
|
||
const monthlyData = <?php echo json_encode(array_values($monthlyData)); ?>;
|
||
const monthlyImagesData = <?php echo json_encode(array_values($monthlyImagesData)); ?>;
|
||
const hourlyData = <?php echo json_encode(array_values($hourlyData)); ?>;
|
||
const hourlyLabels = <?php echo json_encode(array_keys($hourlyData)); ?>;
|
||
const weeklyData = <?php echo json_encode(array_values($weeklyData)); ?>;
|
||
const weeklyLabels = <?php echo json_encode(array_keys($weeklyData)); ?>;
|
||
const categoryLabels = <?php echo json_encode(array_column($categoryDistribution, 'name')); ?>;
|
||
const categoryData = <?php echo json_encode(array_column($categoryDistribution, 'count')); ?>;
|
||
const categoryColors = ['#667eea','#f093fb','#4facfe','#43e97b','#fa709a','#fee140','#a8edea','#fed6e3'];
|
||
|
||
// 准备饼图工具提示回调函数
|
||
function createTooltipCallback() {
|
||
return {
|
||
callbacks: {
|
||
label: function(context) {
|
||
const value = context.parsed;
|
||
const data = context.dataset.data;
|
||
const total = data.reduce((sum, val) => sum + val, 0);
|
||
const percentage = total > 0 ? Math.round((value / total) * 100) + '%' : '0%';
|
||
return `${value} (${percentage})`;
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
// 月度文章发布图表
|
||
new Chart(document.getElementById('monthlyChart'), {
|
||
type: 'line',
|
||
data: {
|
||
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
||
datasets: [{
|
||
label: '文章数',
|
||
data: monthlyData,
|
||
borderColor: chartColor,
|
||
backgroundColor: chartColor + '20',
|
||
fill: true,
|
||
tension: 0.4
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
callbacks: {
|
||
title: function(tooltipItems) {
|
||
const monthIndex = tooltipItems[0].dataIndex;
|
||
const count = monthlyData[monthIndex];
|
||
return (monthIndex + 1) + '月 (' + count + ')';
|
||
},
|
||
label: function(context) {
|
||
return '文章数: ' + context.parsed.y;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
stepSize: 1,
|
||
callback: function(value) {
|
||
return Number.isInteger(value) ? value : '';
|
||
}
|
||
}
|
||
},
|
||
x: {
|
||
ticks: {
|
||
callback: function(value, index) {
|
||
// 在X轴标签上显示文章数量
|
||
return (index + 1) + '月 (' + monthlyData[index] + ')';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 月度图片上传图表
|
||
new Chart(document.getElementById('monthlyImagesChart'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
|
||
datasets: [{
|
||
label: '图片数',
|
||
data: monthlyImagesData,
|
||
backgroundColor: '#f093fb',
|
||
borderColor: '#f093fb',
|
||
borderWidth: 1
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false },
|
||
tooltip: {
|
||
callbacks: {
|
||
title: function(tooltipItems) {
|
||
const monthIndex = tooltipItems[0].dataIndex;
|
||
const count = monthlyImagesData[monthIndex];
|
||
return (monthIndex + 1) + '月 (' + count + ')';
|
||
},
|
||
label: function(context) {
|
||
return '图片数: ' + context.parsed.y;
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
stepSize: 1,
|
||
callback: function(value) {
|
||
return Number.isInteger(value) ? value : '';
|
||
}
|
||
}
|
||
},
|
||
x: {
|
||
ticks: {
|
||
callback: function(value, index) {
|
||
// 在X轴标签上显示图片数量
|
||
return (index + 1) + '月 (' + monthlyImagesData[index] + ')';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 发布时段图表
|
||
new Chart(document.getElementById('hourlyChart'), {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: hourlyLabels,
|
||
datasets: [{
|
||
data: hourlyData,
|
||
backgroundColor: ['#667eea','#f093fb','#4facfe','#43e97b']
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
position: 'bottom',
|
||
labels: {
|
||
padding: 20
|
||
}
|
||
},
|
||
tooltip: createTooltipCallback()
|
||
},
|
||
cutout: '60%'
|
||
}
|
||
});
|
||
|
||
<?php if (!empty($weeklyData)): ?>
|
||
new Chart(document.getElementById('weeklyChart'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: weeklyLabels,
|
||
datasets: [{
|
||
label: '文章数',
|
||
data: weeklyData,
|
||
backgroundColor: chartColor + '80',
|
||
borderColor: chartColor,
|
||
borderWidth: 1
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: { display: false }
|
||
},
|
||
scales: {
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {
|
||
stepSize: 1,
|
||
callback: function(value) {
|
||
return Number.isInteger(value) ? value : '';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
<?php endif; ?>
|
||
|
||
<?php if (!empty($categoryDistribution)): ?>
|
||
new Chart(document.getElementById('categoryChart'), {
|
||
type: 'pie',
|
||
data: {
|
||
labels: categoryLabels,
|
||
datasets: [{
|
||
data: categoryData,
|
||
backgroundColor: categoryColors
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
position: 'bottom',
|
||
labels: {
|
||
padding: 20
|
||
}
|
||
},
|
||
tooltip: createTooltipCallback()
|
||
}
|
||
}
|
||
});
|
||
<?php endif; ?>
|
||
</script>
|
||
|
||
<?php
|
||
include 'copyright.php';
|
||
include 'common-js.php';
|
||
include 'footer.php';
|
||
?>
|