白荼日记 - PHP https://blog.iletter.top/tag/PHP/ zh-CN Tue, 28 Oct 2025 19:29:00 +0800 Tue, 28 Oct 2025 19:29:00 +0800 typecho添加附件功能如果是图片就可以预览 https://blog.iletter.top/archives/492/ https://blog.iletter.top/archives/492/ Tue, 28 Oct 2025 19:29:00 +0800 DelLevin 定位文件file-upload.phpfile-upload-js.php。修改之前记得备份文件。更改了一下排序顺序,刚上传的放在最前面。如果是图片就可以简单预览缩略图。

微信图片_20251028192723_266_15.png

更改file-upload.php

<?php if(!defined('__TYPECHO_ADMIN__')) exit; ?>

<?php
if (isset($post) || isset($page)) {
    $cid = isset($post) ? $post->cid : $page->cid;

    if ($cid) {
        \Widget\Contents\Attachment\Related::alloc(['parentId' => $cid])->to($attachment);
    } else {
        \Widget\Contents\Attachment\Unattached::alloc()->to($attachment);
    }
}
?>

<div id="upload-panel" class="p">
    <div class="upload-area" draggable="true"><?php _e('拖放文件到这里<br>或者 %s选择文件上传%s', '<a href="###" class="upload-file">', '</a>'); ?></div>
    <ul id="file-list">
    <?php while ($attachment->next()): ?>
        <li data-cid="<?php $attachment->cid(); ?>" data-url="<?php echo $attachment->attachment->url; ?>" data-image="<?php echo $attachment->attachment->isImage ? 1 : 0; ?>">
            <input type="hidden" name="attachment[]" value="<?php $attachment->cid(); ?>" />
            <a class="insert" title="<?php _e('点击插入文件'); ?>" href="###"><?php $attachment->title(); ?></a>
            <div class="info">
                <?php echo number_format(ceil($attachment->attachment->size / 1024)); ?> Kb
                <a class="file" target="_blank" href="<?php $options->adminUrl('media.php?cid=' . $attachment->cid); ?>" title="<?php _e('编辑'); ?>"><i class="i-edit"></i>编辑</a>
                <a href="###" class="delete" title="<?php _e('删除'); ?>"><i class="i-delete"></i>删除</a>
            </div>
            <?php if ($attachment->attachment->isImage): ?>
                <div class="image-preview">
                    <img src="<?php echo $attachment->attachment->url; ?>" alt="<?php $attachment->title(); ?>" />
                </div>
            <?php endif; ?>
        </li>
    <?php endwhile; ?>
    </ul>
</div>

<style>
/* 为包含图片的列表项设置基础样式 */
#file-list li {
    margin-bottom: 10px; /* 列表项之间的间距 */
    padding: 8px; /* 内边距 */
    border: 1px solid #eee; /* 边框 */
    border-radius: 4px; /* 圆角 */
    background-color: #fafafa; /* 背景色 */
    list-style: none; /* 去除默认列表符号 */
}

/* 标题样式 */
#file-list li .insert {
    display: block; /* 使其独占一行 */
    font-weight: bold; /* 标题加粗 */
    margin-bottom: 4px; /* 与下方 .info 的间距 */
    color: #333; /* 标题颜色 */
    text-decoration: none; /* 去除下划线 */
}

#file-list li .insert:hover {
    text-decoration: underline; /* 悬停时添加下划线 */
}

/* 信息栏样式 */
#file-list li .info {
    font-size: 0.9em; /* 信息栏字体稍小 */
    color: #666; /* 信息栏颜色 */
    margin-bottom: 8px; /* 与下方图片的间距 */
    line-height: 1.4; /* 行高 */
}

/* 信息栏内的链接和图标 */
#file-list li .info a {
    margin-right: 8px; /* 链接之间的间距 */
    color: #999; /* 图标颜色 */
    text-decoration: none;
}

#file-list li .info a:hover {
    color: #007cba; /* 悬停时的颜色 */
}

/* 图片预览容器 */
.image-preview {
    text-align: center; /* 图片居中 */
    margin-top: 5px; /* 与上方 .info 的间距 */
    clear: both; /* 清除浮动(如果有的话) */
}

/* 图片样式 */
.image-preview img {
    max-width: 150px; /* 设置最大宽度 */
    max-height: 150px; /* 设置最大高度 */
    height: auto; /* 保持宽高比 */
    border: 1px solid #ddd; /* 图片边框 */
    border-radius: 4px; /* 图片圆角 */
    padding: 2px; /* 图片内边距 */
    background-color: #fff; /* 图片背景色,防止透明图有背景色干扰 */
    box-shadow: 0 1px 3px rgba(0,0,0,0.1); /* 添加轻微阴影 */
}

/* 加载状态样式 */
#file-list li.loading {
    color: #999;
    font-style: italic;
}

/* 删除按钮悬停效果 */
#file-list li a.delete:hover {
    color: #e74c3c !important; /* 删除按钮悬停时变为红色 */
}

/* 编辑按钮悬停效果 */
#file-list li a.file:hover {
    color: #3498db !important; /* 编辑按钮悬停时变为蓝色 */
}
</style>

更改file-upload-js.php

<?php if(!defined('__TYPECHO_ADMIN__')) exit; ?>
<?php
if (isset($post) && $post instanceof \Typecho\Widget && $post->have()) {
    $fileParentContent = $post;
} elseif (isset($page) && $page instanceof \Typecho\Widget && $page->have()) {
    $fileParentContent = $page;
}

$phpMaxFilesize = function_exists('ini_get') ? trim(ini_get('upload_max_filesize')) : 0;

if (preg_match("/^([0-9]+)([a-z]{1,2})$/i", $phpMaxFilesize, $matches)) {
    $phpMaxFilesize = strtolower($matches[1] . $matches[2] . (1 == strlen($matches[2]) ? 'b' : ''));
}
?>

<script src="<?php $options->adminStaticUrl('js', 'moxie.js'); ?>"></script>
<script src="<?php $options->adminStaticUrl('js', 'plupload.js'); ?>"></script>
<script>
$(document).ready(function() {
    function updateAttacmentNumber () {
        var btn = $('#tab-files-btn'),
            balloon = $('.balloon', btn),
            count = $('#file-list li .insert').length;

        if (count > 0) {
            if (!balloon.length) {
                btn.html($.trim(btn.html()) + ' ');
                balloon = $('<span class="balloon"></span>').appendTo(btn);
            }

            balloon.html(count);
        } else if (0 == count && balloon.length > 0) {
            balloon.remove();
        }
    }

    $('.upload-area').bind({
        dragenter   :   function () {
            $(this).parent().addClass('drag');
        },

        dragover    :   function (e) {
            $(this).parent().addClass('drag');
        },

        drop        :   function () {
            $(this).parent().removeClass('drag');
        },
        
        dragend     :   function () {
            $(this).parent().removeClass('drag');
        },

        dragleave   :   function () {
            $(this).parent().removeClass('drag');
        }
    });

    updateAttacmentNumber();

    function fileUploadStart (file) {
        $('<li id="' + file.id + '" class="loading">'
            + file.name + '</li>').appendTo('#file-list');
    }

    function fileUploadError (error) {
        var file = error.file, code = error.code, word; 
        
        switch (code) {
            case plupload.FILE_SIZE_ERROR:
                word = '<?php _e('文件大小超过限制'); ?>';
                break;
            case plupload.FILE_EXTENSION_ERROR:
                word = '<?php _e('文件扩展名不被支持'); ?>';
                break;
            case plupload.FILE_DUPLICATE_ERROR:
                word = '<?php _e('文件已经上传过'); ?>';
                break;
            case plupload.HTTP_ERROR:
            default:
                word = '<?php _e('上传出现错误'); ?>';
                break;
        }

        var fileError = '<?php _e('%s 上传失败'); ?>'.replace('%s', file.name),
            li, exist = $('#' + file.id);

        if (exist.length > 0) {
            li = exist.removeClass('loading').html(fileError);
        } else {
            li = $('<li>' + fileError + '<br />' + word + '</li>').appendTo('#file-list');
        }

        li.effect('highlight', {color : '#FBC2C4'}, 2000, function () {
            $(this).remove();
        });

        // fix issue #341
        this.removeFile(file);
    }

    var completeFile = null;
    function fileUploadComplete (id, url, data) {
        // 根据是否为图片来构建列表项内容
        var itemContent = '<input type="hidden" name="attachment[]" value="' + data.cid + '" />';
    
        // 添加标题和信息
        itemContent += '<a class="insert" target="_blank" href="###" title="<?php _e('点击插入文件'); ?>">' + data.title + '</a>'
                     + '<div class="info">' + data.bytes
                     + ' <a class="file" target="_blank" href="<?php $options->adminUrl('media.php'); ?>?cid=' 
                     + data.cid + '" title="<?php _e('编辑'); ?>"><i class="i-edit"></i>编辑</a>'
                     + ' <a class="delete" href="###" title="<?php _e('删除'); ?>"><i class="i-delete"></i>删除</a></div>';
    
        // 如果是图片,添加预览图 (注意:这里图片放在 .info 之后,与PHP模板保持一致)
        if (data.isImage) {
            itemContent += '<div class="image-preview"><img src="' + data.url + '" alt="' + data.title + '" /></div>';
        }
    
        // 创建 jQuery 对象 li
        var li = $('#' + id).removeClass('loading').data('cid', data.cid)
            .data('url', data.url)
            .data('image', data.isImage)
            .html(itemContent); // 先设置内容
    
        // 关键修改:将新 li 插入到 #file-list 的最前面,而不是留在原地或追加到末尾
        // 1. 先从当前位置移除(如果它在列表中的话,虽然通常在上传开始时是添加到列表末尾的空li)
        // 2. 然后插入到 #file-list 的开头
        li.prependTo('#file-list'); // prependTo 将元素插入到目标元素的开头
    
        // 绑定事件
        attachInsertEvent(li);
        attachDeleteEvent(li);
        updateAttacmentNumber();
    
        if (!completeFile) {
            completeFile = data;
        }
    }

    var uploader = null, tabFilesEl = $('#tab-files').bind('init', function () {
        uploader = new plupload.Uploader({
            browse_button   :   $('.upload-file').get(0),
            url             :   '<?php $security->index('/action/upload'
                . (isset($fileParentContent) ? '?cid=' . $fileParentContent->cid : '')); ?>',
            runtimes        :   'html5,flash,html4',
            flash_swf_url   :   '<?php $options->adminStaticUrl('js', 'Moxie.swf'); ?>',
            drop_element    :   $('.upload-area').get(0),
            filters         :   {
                max_file_size       :   '<?php echo $phpMaxFilesize ?>',
                mime_types          :   [{'title' : '<?php _e('允许上传的文件'); ?>', 'extensions' : '<?php echo implode(',', $options->allowedAttachmentTypes); ?>'}],
                prevent_duplicates  :   true
            },

            init            :   {
                FilesAdded      :   function (up, files) {
                    for (var i = 0; i < files.length; i ++) {
                        fileUploadStart(files[i]);
                    }

                    completeFile = null;
                    uploader.start();
                },

                UploadComplete  :   function () {
                    if (completeFile) {
                        Typecho.uploadComplete(completeFile);
                    }
                },

                FileUploaded    :   function (up, file, result) {
                    if (200 == result.status) {
                        var data = $.parseJSON(result.response);

                        if (data) {
                            fileUploadComplete(file.id, data[0], data[1]);
                            uploader.removeFile(file);
                            return;
                        }
                    }

                    fileUploadError.call(uploader, {
                        code : plupload.HTTP_ERROR,
                        file : file
                    });
                },

                Error           :   function (up, error) {
                    fileUploadError.call(uploader, error);
                }
            }
        });

        uploader.init();
    });

    Typecho.uploadFile = function (file, name) {
        if (!uploader) {
            $('#tab-files-btn').parent().trigger('click');
        }
        
        var timer = setInterval(function () {
            if (!uploader) {
                return;
            }

            clearInterval(timer);
            timer = null;

            uploader.addFile(file, name);
        }, 50);
    };

    // function attachInsertEvent (el) {
    //     $('.insert', el).click(function () {
    //         var t = $(this), p = t.parents('li');
    //         Typecho.insertFileToEditor(t.text(), p.data('url'), p.data('image'));
    //         return false;
    //     });
    // }
    // 修改 attachInsertEvent 函数,使其能处理标题链接和图片
    function attachInsertEvent (el) {
        // 为标题链接和图片(或其父容器 .image-preview)绑定点击事件
        $('.insert, .image-preview img', el).click(function (e) {
            // 防止事件冒泡到父级 <a> 标签(如果图片被另一个链接包裹的话)
            e.stopPropagation();
            
            var t = $(this);
            // 查找当前点击元素的父级 <li>,然后从中获取数据
            var p = t.closest('li'); // 使用 closest 更可靠,可以找到最近的祖先 <li>
    
            // 确保找到了包含数据的 <li> 元素
            if (p.length > 0) {
                // 从 <li> 元素获取数据
                var url = p.data('url');
                var isImage = p.data('image');
                var title = p.find('.insert').first().text(); // 获取标题文本
    
                // 调用 Typecho 提供的插入函数
                Typecho.insertFileToEditor(title, url, isImage);
            }
            
            return false; // 阻止默认链接行为
        });
    }

    function attachDeleteEvent (el) {
        var file = $('a.insert', el).text();
        $('.delete', el).click(function () {
            if (confirm('<?php _e('确认要删除文件 %s 吗?'); ?>'.replace('%s', file))) {
                var cid = $(this).parents('li').data('cid');
                $.post('<?php $security->index('/action/contents-attachment-edit'); ?>',
                    {'do' : 'delete', 'cid' : cid},
                    function () {
                        $(el).fadeOut(function () {
                            $(this).remove();
                            updateAttacmentNumber();
                        });
                    });
            }

            return false;
        });
    }

    $('#file-list li').each(function () {
        attachInsertEvent(this);
        attachDeleteEvent(this);
    });
});
</script>


]]>
0 https://blog.iletter.top/archives/492/#comments https://blog.iletter.top/feed/tag/PHP/
让typecho的access插件使用ip2region 地址库 https://blog.iletter.top/archives/459/ https://blog.iletter.top/archives/459/ Thu, 28 Aug 2025 22:42:00 +0800 DelLevin 旧版本的typecho的access插件记录的ip太过于落后了。判断也有很多问题,所以需要在此更新一下。

定位到地址文件是lib下面的ipipfree.ipdb,删掉这个就好,用不到了。这个最近的时间是2019年的。真的很无语的。

接下来定位到所在位置是Access_Core.php这个文件的这里代码。(Access_IpDb.php这个也可以删除)

            try {
                $ipdb = new Access_IpDb(dirname(__file__).'/lib/ipipfree.ipdb');
                $city = $ipdb->findInfo($ip, 'CN');
                 // 写入日志
                error_log("IP: {$ip}\nCity Info: " . print_r($city, true), 3, '/tmp/access_debug.log');
                $ip_country = $city->country_name;
                if($ip_country == '中国') {
                    $ip_province = $city->region_name;
                    $ip_city = $city->city_name;
                } else {
                    $ip_province = $ip_city = NULL;
                }
            } catch(Exception $e) {
                $ip_country = $ip_province = $ip_city = '未知';
            }
            
            

然后根据数据格式,改成他的数据样式的。

try {
                // 检查必要的文件是否存在
                $dbFile = dirname(__FILE__) . '/lib/ip2region.xdb';
                $classFile = dirname(__FILE__) . '/lib/XdbSearcher.php';
                
                if (!file_exists($dbFile)) {
                    throw new Exception("Ip2region database file not found: {$dbFile}");
                }
                
                if (!file_exists($classFile)) {
                    throw new Exception("XdbSearcher class file not found: {$classFile}");
                }
                
                require_once $classFile;
                
                $searcher = XdbSearcher::newWithFileOnly($dbFile);
                $region = $searcher->search($ip);
                
                if ($region === null) {
                    throw new Exception("IP2Region search failed for IP: {$ip}");
                }
                
                // 调试日志
                error_log("IP: {$ip}\nRegion Info: " . print_r($region, true), 3, '/tmp/access_debug.log');
                
                // 解析数据 (格式: 国家|区域|省份|城市|ISP)
                $regionArray = explode('|', $region);
                
                // 清理数据
                $cleanData = function($data) {
                    return (!empty($data) && $data !== '0') ? $data : '';
                };
                
                $country = $cleanData($regionArray[0]);
                $region_info = $cleanData($regionArray[1]);
                $province = $cleanData($regionArray[2]);
                $city = $cleanData($regionArray[3]);
                
                // 设置最终保存到数据库的字段
                $ip_country = !empty($country) ? $country : '未知';
                
                // 无论国内外都保存区域和城市信息
                $ip_province = '';
                $ip_city = '';
                
                if (!empty($province)) {
                    $ip_province = $province;
                } elseif (!empty($region_info)) {
                    // 如果省份为空,但区域不为空,可以用区域代替
                    $ip_province = $region_info;
                }
                
                if (!empty($city)) {
                    $ip_city = $city;
                }
                
            } catch(Exception $e) {
                error_log("IP解析异常:" . $e->getMessage(), 3, '/tmp/access_debug.log');
                $ip_country = '未知';
                $ip_province = '';
                $ip_city = '';
            }

这样就可以了。

]]>
0 https://blog.iletter.top/archives/459/#comments https://blog.iletter.top/feed/tag/PHP/
typecho文章编辑界面新增参考文章功能 https://blog.iletter.top/archives/450/ https://blog.iletter.top/archives/450/ Mon, 25 Aug 2025 22:47:00 +0800 DelLevin 因为再写技术博客的时候总是要很多的参考文章。直接在文章引用感觉怪怪的。所以自己更改了一些数据结构,接口。

效果为这样的:

微信图片_2025-08-25_223652_204.png#B #S #R #60%

增加数据库字段

typecho_contents这个表增加一个字段post_links,类型为text

修改接口文件

路径在var/Widget/Contents/Post下面的Edit.php文件

修改编辑文章文件

文件地址在admin路径下的write-post.php文件,新增部分。css暂时不写了

                    <!-- 引用文章开始 -->
                    <section class="typecho-post-option reference-links-section" id="reference-links-section">
                        <label class="typecho-label toggle-reference-section" style="cursor: pointer;">
                            <span class="toggle-icon" style="float: left;"><i class="i-caret-right"></i></span>
                             <?php _e('参考文章'); ?>
                        </label>
                        <div class="reference-links-container" id="post-links-container">
                            <?php
                            // 从数据库获取数据
                            $links = [];
                            if ($post->have()) {
                                $db = Typecho\Db::get();
                                $result = $db->fetchRow($db->select('post_links')->from('table.contents')->where('cid = ?', $post->cid));
                                if (!empty($result) && !empty($result['post_links'])) {
                                    $postLinks = $result['post_links'];
                                    $links = json_decode($postLinks, true);
                                    if (!is_array($links)) $links = [];
                                }
                            }
                            
                            // 如果没有数据,添加一个空行
                            if (empty($links)) {
                                $links = [['name' => '', 'link' => '']];
                            }
                            
                            foreach ($links as $index => $link):
                            ?>
                            <div class="reference-link-item">
                                <div class="reference-link-inputs">
                                    <input type="text" 
                                           name="post_links[<?php echo $index; ?>][name]" 
                                           placeholder="<?php _e('链接名称'); ?>" 
                                           value="<?php echo htmlspecialchars($link['name'] ?? ''); ?>" 
                                           class="text reference-link-name" />
                                    <input type="url" 
                                           name="post_links[<?php echo $index; ?>][link]" 
                                           placeholder="<?php _e('https://'); ?>" 
                                           value="<?php echo htmlspecialchars($link['link'] ?? ''); ?>" 
                                           class="text reference-link-url" />
                                </div>
                                <div class="reference-link-actions">
                                    <button type="button" class="btn btn-xs remove-reference-link" title="<?php _e('删除'); ?>">
                                        <i class="i-delete"></i>
                                    </button>
                                </div>
                            </div>
                            <?php endforeach; ?>
                        </div>
                        <div class="reference-links-footer">
                            <button type="button" id="add-reference-link" class="btn btn-xs">
                                <i class="i-plus"></i> <?php _e('添加引用'); ?>
                            </button>
                            <span class="reference-links-help"><?php _e('温故而知新,添加相关的参考链接,记忆更牢固。'); ?></span>
                        </div>
                    </section>
                    <!-- 引用文章结束 -->

受限于代码长度过长,使得文章无法正常解析,可以点击链接下载。

点击下载:

代码.zip

]]>
2 https://blog.iletter.top/archives/450/#comments https://blog.iletter.top/feed/tag/PHP/
typecho美化文件管理界面 https://blog.iletter.top/archives/447/ https://blog.iletter.top/archives/447/ Mon, 25 Aug 2025 15:49:00 +0800 DelLevin 后台界面太丑陋, 我就自己在这里改了改。添加文件预览以及预览图片放大。

更改#B #S #R #60%

预览图片#B #S #R #60%

上传文件#B #S #R #60%

添加接口

因为后台接口少了统计接口,所以这里需要手动后台添加。目录在安装目录下面的/var/Widget/Contents/Attachment这个目录, 然后编辑Admin.php。复制以下代码即可。

<?php

namespace Widget\Contents\Attachment;

use Typecho\Config;
use Typecho\Db;
use Typecho\Db\Exception;
use Typecho\Db\Query;
use Typecho\Widget\Helper\PageNavigator\Box;
use Widget\Base\Contents;
use Typecho\Common;

if (!defined('__TYPECHO_ROOT_DIR__')) {
    exit;
}

/**
 * 文件管理列表组件
 *
 * @category typecho
 * @package Widget
 * @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
 * @license GNU General Public License 2.0
 */
class Admin extends Contents
{
    /**
     * 用于计算数值的语句对象
     *
     * @var Query
     */
    private $countSql;

    /**
     * 所有文章个数
     *
     * @var integer
     */
    private $total = false;

    /**
     * 当前页
     *
     * @var integer
     */
    private $currentPage;

    /**
     * 执行函数
     *
     * @return void
     * @throws Exception|\Typecho\Widget\Exception
     */
    public function execute()
    {
        $this->parameter->setDefault('pageSize=20');
        $this->currentPage = $this->request->get('page', 1);

        /** 构建基础查询 */
        $select = $this->select()->where('table.contents.type = ?', 'attachment');

        /** 如果具有编辑以上权限,可以查看所有文件,反之只能查看自己的文件 */
        if (!$this->user->pass('editor', true)) {
            $select->where('table.contents.authorId = ?', $this->user->uid);
        }

        /** 过滤标题 */
        if (null != ($keywords = $this->request->filter('search')->keywords)) {
            $args = [];
            $keywordsList = explode(' ', $keywords);
            $args[] = implode(' OR ', array_fill(0, count($keywordsList), 'table.contents.title LIKE ?'));

            foreach ($keywordsList as $keyword) {
                $args[] = '%' . $keyword . '%';
            }

            call_user_func_array([$select, 'where'], $args);
        }

        /** 给计算数目对象赋值,克隆对象 */
        $this->countSql = clone $select;

        /** 提交查询 */
        $select->order('table.contents.created', Db::SORT_DESC)
            ->page($this->currentPage, $this->parameter->pageSize);

        $this->db->fetchAll($select, [$this, 'push']);
    }

    /**
     * 输出分页
     *
     * @return void
     * @throws Exception|\Typecho\Widget\Exception
     */
    public function pageNav()
    {
        $query = $this->request->makeUriByRequest('page={page}');

        /** 使用盒状分页 */
        $nav = new Box(
            false === $this->total ? $this->total = $this->size($this->countSql) : $this->total,
            $this->currentPage,
            $this->parameter->pageSize,
            $query
        );

        $nav->render('&laquo;', '&raquo;');
    }

    /**
     * 所属文章
     *
     * @return Config
     * @throws Exception
     */
    protected function ___parentPost(): Config
    {
        return new Config($this->db->fetchRow(
            $this->select()->where('table.contents.cid = ?', $this->parentId)->limit(1)
        ));
    }

    /**
     * 获取附件的 URL 地址
     *
     * @return string
     */
    protected function ___attachmentUrl(): string
    {
        if (!empty($this->attachment) && !empty($this->attachment->path)) {
            // 从 path 字段构建 URL
            $path = $this->attachment->path;
            // 移除开头的斜杠(如果有的话)
            $path = ltrim($path, '/');
            return $this->options->siteUrl . $path;
        }
        
        // 如果 path 不存在,尝试从 text 字段解析
        if (!empty($this->___attachment())) {
            $attachment = $this->___attachment();
            if (!empty($attachment['path'])) {
                $path = ltrim($attachment['path'], '/');
                return $this->options->siteUrl . $path;
            }
        }
        
        return '';
    }

    /**
     * 解析附件信息
     *
     * @return array
     */
    protected function ___attachment(): array
    {
        if (!empty($this->text)) {
            $attachment = @unserialize($this->text);
            if (is_array($attachment)) {
                return $attachment;
            }
        }
        return [];
    }

    /**
     * 重写 permalink 方法,使用解析后的 URL
     *
     * @return string
     */
    protected function ___permalink(): string
    {
        return $this->___attachmentUrl();
    }

    /**
     * 获取统计信息
     *
     * @return array
     * @throws Exception
     */
    public function getStats(): array
    {
        $select = $this->select()->where('table.contents.type = ?', 'attachment');
        
        /** 如果具有编辑以上权限,可以查看所有文件,反之只能查看自己的文件 */
        if (!$this->user->pass('editor', true)) {
            $select->where('table.contents.authorId = ?', $this->user->uid);
        }

        // 总文件数
        $total = $this->size(clone $select);

        // 图片文件数
        $imageSelect = clone $select;
        $imageSelect->join('table.fields', 'table.contents.cid = table.fields.cid', Db::LEFT_JOIN)
                   ->where('table.fields.name = ?', 'mime')
                   ->where('table.fields.str_value LIKE ?', 'image/%');
        $imageCount = $this->size($imageSelect);

        // 未归档文件数
        $unattachedSelect = clone $select;
        $unattachedSelect->where('table.contents.parent = ?', 0);
        $unattachedCount = $this->size($unattachedSelect);

        return [
            'total' => $total,
            'images' => $imageCount,
            'unattached' => $unattachedCount
        ];
    }

    /**
     * 获取总文件数
     *
     * @return int
     * @throws Exception
     */
    public function getTotalCount(): int
    {
        $select = $this->select()->where('table.contents.type = ?', 'attachment');
        
        /** 如果具有编辑以上权限,可以查看所有文件,反之只能查看自己的文件 */
        if (!$this->user->pass('editor', true)) {
            $select->where('table.contents.authorId = ?', $this->user->uid);
        }

        return $this->size($select);
    }

    /**
     * 获取图片文件数
     *
     * @return int
     * @throws Exception
     */
    public function getImageCount(): int
    {
        $select = $this->select()->where('table.contents.type = ?', 'attachment');
        
        /** 如果具有编辑以上权限,可以查看所有文件,反之只能查看自己的文件 */
        if (!$this->user->pass('editor', true)) {
            $select->where('table.contents.authorId = ?', $this->user->uid);
        }

        // 通过 MIME 类型过滤图片
        $select->where('table.contents.text LIKE ?', '%image/%');
        
        return $this->size($select);
    }

    /**
     * 获取未归档文件数
     *
     * @return int
     * @throws Exception
     */
    public function getUnattachedCount(): int
    {
        $select = $this->select()->where('table.contents.type = ?', 'attachment');
        
        /** 如果具有编辑以上权限,可以查看所有文件,反之只能查看自己的文件 */
        if (!$this->user->pass('editor', true)) {
            $select->where('table.contents.authorId = ?', $this->user->uid);
        }

        // 未归档文件(parent = 0)
        $select->where('table.contents.parent = ?', 0);
        
        return $this->size($select);
    }
}

更改后台文件界面

通过查看后台路径可以得知是manage-medias.php这个文件,所以更改一下这个文件就可以了。更改前注意文件备份。

<?php
include 'common.php';
include 'header.php';
include 'menu.php';

$stat = \Widget\Stat::alloc();
$attachments = \Widget\Contents\Attachment\Admin::alloc();

// 获取统计信息
$totalCount = $attachments->getTotalCount();
$imageCount = $attachments->getImageCount();
$unattachedCount = $attachments->getUnattachedCount();

// 获取PHP上传限制
$phpMaxFilesize = function_exists('ini_get') ? trim(ini_get('upload_max_filesize')) : 0;
if (preg_match("/^([0-9]+)([a-z]{1,2})$/i", $phpMaxFilesize, $matches)) {
    $phpMaxFilesize = strtolower($matches[1] . $matches[2] . (1 == strlen($matches[2]) ? 'b' : ''));
}
?>
<style>
/* 上传区域样式 */
.upload-section {
    background: #fff;
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 20px;
    margin: 20px 0;
    display: none;
}

.upload-section.show {
    display: block;
}

.upload-section h3 {
    margin: 0 0 15px 0;
    padding: 0;
    font-size: 16px;
    color: #333;
}

.upload-controls {
    display: flex;
    gap: 15px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 15px;
}

/* 改为链接样式 */
.upload-link {
    color: #007cba;
    text-decoration: underline;
    cursor: pointer;
    font-size: 14px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    transition: color 0.2s;
}

.upload-link:hover {
    color: #005a87;
    text-decoration: none;
}

.upload-link i {
    font-size: 16px;
}

.upload-hint {
    color: #666;
    font-size: 13px;
}

.upload-area {
    border: 2px dashed #ddd;
    border-radius: 6px;
    padding: 30px;
    text-align: center;
    background: #fafafa;
    transition: all 0.3s;
    margin-top: 10px;
}

.upload-area.dragover {
    border-color: #007cba;
    background: #f0f8ff;
}

.upload-area-text {
    color: #666;
    margin-bottom: 10px;
}

.browse-link {
    color: #007cba;
    text-decoration: underline;
    cursor: pointer;
}

.browse-link:hover {
    color: #005a87;
}

/* 上传进度条 */
.upload-progress {
    display: none;
    margin-top: 15px;
}

.progress-bar {
    width: 100%;
    height: 20px;
    background: #f0f0f0;
    border-radius: 10px;
    overflow: hidden;
    
    
}

.progress-fill {
    height: 100%;
    background: #007cba;
    width: 0%;
    transition: width 0.3s;
}

.progress-text {
    text-align: center;
    font-size: 12px;
    color: #666;
    margin-top: 5px;
}


.media-stats {
    background: #fff;
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 15px;
    margin-bottom: 20px;
}

.media-stats h3 {
    margin: 0 0 15px 0;
    padding: 0;
    font-size: 16px;
    color: #666;
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 15px;
}

.stat-item {
    text-align: center;
    padding: 10px;
    background: #f8f9fa;
    border-radius: 4px;
}

.stat-number {
    font-size: 24px;
    font-weight: bold;
    color: #007cba;
    margin-bottom: 5px;
}

.stat-label {
    font-size: 13px;
    color: #666;
}

/* 预览列样式 */
.preview-cell {
    text-align: center;
}

.media-preview-img {
    width: 50px;
    height: 50px;
    object-fit: cover;
    border-radius: 4px;
    border: 1px solid #ddd;
    cursor: pointer;
    transition: transform 0.2s ease;
}

.media-preview-img:hover {
    transform: scale(1.1);
    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}

.media-preview-icon {
    font-size: 24px;
    color: #999;
}

/* 保持原有样式 */
.typecho-list-operate {
    background: #fff;
    border: 1px solid #d9d9d9;
    border-radius: 4px;
    padding: 15px;
    margin-bottom: 20px;
}

.typecho-pager {
    margin-top: 20px;
    text-align: center;
}

/* 图片预览模态框样式 */
.modal {
    display: none;
    position: fixed;
    z-index: 10000;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0,0,0,0.8);
    backdrop-filter: blur(5px);
}

.modal-content {
    position: relative;
    margin: 5% auto;
    padding: 20px;
    width: 90%;
    max-width: 900px;
    max-height: 90vh;
    text-align: center;
}

.modal-image {
    max-width: 100%;
    max-height: 80vh;
    object-fit: contain;
    border-radius: 8px;
    box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}

.close {
    position: absolute;
    top: 10px;
    right: 25px;
    color: #fff;
    font-size: 35px;
    font-weight: bold;
    cursor: pointer;
    z-index: 10001;
}

.close:hover {
    color: #ccc;
}

.modal-title {
    color: white;
    margin-top: 10px;
    font-size: 16px;
    font-weight: normal;
}

/* 上传按钮样式 - 与清理按钮统一 */
.btn-upload {
    background: #28a745;
    border-color: #28a745;
    color: white;
    margin-left: 10px;
}

.btn-upload:hover {
    background: #218838;
    border-color: #1e7e34;
}

/* 取消按钮样式 */
#cancelUploadBtn {
    margin-left: 10px;
}

/* 隐藏的文件输入框 */
#fileInput {
    display: none;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }
    
    .modal-content {
        width: 95%;
        margin: 10% auto;
    }
    
    .modal-image {
        max-height: 70vh;
    }
    
    .upload-controls {
        flex-direction: column;
        align-items: stretch;
    }
    
    .btn-upload {
        margin-left: 0;
        margin-top: 5px;
    }
}
</style>

<div class="main">
    <div class="body container">
        <?php include 'page-title.php'; ?>
        <div class="row typecho-page-main" role="main">
            <div class="col-mb-12">

                <!-- 统计信息 -->
                <div class="media-stats">
                    <h3>媒体文件统计</h3>
                    <div class="stats-grid">
                        <div class="stat-item">
                            <div class="stat-number"><?php echo $totalCount; ?></div>
                            <div class="stat-label">总文件数</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-number"><?php echo $imageCount; ?></div>
                            <div class="stat-label">图片文件</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-number"><?php echo $stat->publishedPostsNum; ?></div>
                            <div class="stat-label">文章数</div>
                        </div>
                        <div class="stat-item">
                            <div class="stat-number"><?php echo $unattachedCount; ?></div>
                            <div class="stat-label">未归档文件</div>
                        </div>
                    </div>
                </div>

                
                <div class="typecho-list-operate clearfix">
                    <form method="get">
                        <div class="operate">
                            <label><i class="sr-only"><?php _e('全选'); ?></i><input type="checkbox"
                                                                                   class="typecho-table-select-all"/></label>
                            <div class="btn-group btn-drop">
                                <button class="btn dropdown-toggle btn-s" type="button"><i
                                        class="sr-only"><?php _e('操作'); ?></i><?php _e('选中项'); ?> <i
                                        class="i-caret-down"></i></button>
                                <ul class="dropdown-menu">
                                    <li><a lang="<?php _e('你确认要删除这些文件吗?'); ?>"
                                           href="<?php $security->index('/action/contents-attachment-edit?do=delete'); ?>"><?php _e('删除'); ?></a>
                                    </li>
                                </ul>
                                <button class="btn btn-s btn-warn btn-operate"
                                        href="<?php $security->index('/action/contents-attachment-edit?do=clear'); ?>"
                                        lang="<?php _e('您确认要清理未归档的文件吗?'); ?>"><?php _e('清理未归档文件'); ?></button>
                                <!--<button type="button" class="btn btn-s btn-upload" id="toggleUploadBtn">-->
                                <!--    <?php _e('上传文件'); ?>-->
                                <!--</button>-->
                                
                                 <!-- 修改这里:添加条件显示 -->
                                <button type="button" class="btn btn-s btn-upload" id="toggleUploadBtn">
                                    <?php _e('上传文件'); ?>
                                </button>
                                <button type="button" class="btn btn-s" id="cancelUploadBtn" style="display: none;">
                                    <?php _e('取消上传'); ?>
                                </button>
                
                            </div>
                        </div>
                        <div class="search" role="search">
                            <?php if ('' != $request->keywords): ?>
                                <a href="<?php $options->adminUrl('manage-medias.php'); ?>"><?php _e('&laquo; 取消筛选'); ?></a>
                            <?php endif; ?>
                            <input type="text" class="text-s" placeholder="<?php _e('请输入关键字'); ?>"
                                   value="<?php echo $request->filter('html')->keywords; ?>"<?php if ('' == $request->keywords): ?> onclick="value='';name='keywords';" <?php else: ?> name="keywords"<?php endif; ?>/>
                            <button type="submit" class="btn btn-s"><?php _e('筛选'); ?></button>
                        </div>
                    </form>
                </div><!-- end .typecho-list-operate -->
                    
                <!-- 上传区域 - 默认隐藏 -->
                <div class="upload-section" id="uploadSection">
                    <h3><?php _e('上传文件'); ?></h3>
                    <div class="upload-controls">
                        <!-- 改为链接形式 -->
                        <a href="javascript:;" class="upload-link" id="uploadButton">
                            <!--<i class="i-upload"></i> -->
                            <?php _e('选择文件'); ?>
                        </a>
                        <span class="upload-hint"><?php _e('或拖放文件到下方区域'); ?></span>
                    </div>
                    <div class="upload-area" id="uploadArea">
                        <div class="upload-area-text">
                            <?php _e('拖放文件到这里'); ?>
                        </div>
                        <div>
                            <?php _e('或者 %s浏览文件%s', '<span class="browse-link" id="browseLink">', '</span>'); ?>
                        </div>
                    </div>
                    
                    <!-- 上传进度条 -->
                    <div class="upload-progress" id="uploadProgress">
                        <div class="progress-bar">
                            <div class="progress-fill" id="progressFill"></div>
                        </div>
                        <div class="progress-text" id="progressText">0%</div>
                    </div>
                    
                    <!-- 隐藏的文件输入框 -->
                    <input type="file" id="fileInput" multiple style="display: none;">
                </div>

                    
                <form method="post" name="manage_medias" class="operate-form">
                    <div class="typecho-table-wrap">
                        <table class="typecho-list-table draggable">
                            <colgroup>
                                <col width="20" class="kit-hidden-mb"/>
                                <col width="6%" class="kit-hidden-mb"/>
                                <col width="10%" class="kit-hidden-mb"/> <!-- 预览列 -->
                                <col width="25%"/>
                                <col width="" class="kit-hidden-mb"/>
                                <col width="25%" class="kit-hidden-mb"/>
                                <col width="16%"/>
                            </colgroup>
                            <thead>
                            <tr>
                                <th class="kit-hidden-mb"></th>
                                <th class="kit-hidden-mb"></th>
                                <th class="kit-hidden-mb"><?php _e('预览'); ?></th>
                                <th><?php _e('文件名'); ?></th>
                                <th class="kit-hidden-mb"><?php _e('上传者'); ?></th>
                                <th class="kit-hidden-mb"><?php _e('所属文章'); ?></th>
                                <th><?php _e('发布日期'); ?></th>
                            </tr>
                            </thead>
                            <tbody>
                            <?php if ($attachments->have()): ?>
                                <?php while ($attachments->next()): ?>
                                    <?php 
                                        $mime = \Typecho\Common::mimeIconType($attachments->attachment->mime);
                                        $isImage = strpos($attachments->attachment->mime, 'image/') === 0;
                                        $fileUrl = $attachments->attachmentUrl;
                                    ?>
                                    <tr id="<?php $attachments->theId(); ?>">
                                        <td class="kit-hidden-mb">
                                            <input type="checkbox" class="typecho-table-checkbox" 
                                                   value="<?php $attachments->cid(); ?>" name="cid[]"/>
                                        </td>
                                        <td class="kit-hidden-mb"><a
                                                href="<?php $options->adminUrl('manage-comments.php?cid=' . $attachments->cid); ?>"
                                                class="balloon-button size-<?php echo \Typecho\Common::splitByCount($attachments->commentsNum, 1, 10, 20, 50, 100); ?>"><?php $attachments->commentsNum(); ?></a>
                                        </td>
                                        <td class="kit-hidden-mb preview-cell">
                                            <?php if ($isImage && !empty($fileUrl)): ?>
                                                <img src="<?php echo $fileUrl; ?>" 
                                                     class="media-preview-img" 
                                                     alt="<?php $attachments->title(); ?>"
                                                     data-full-src="<?php echo $fileUrl; ?>"
                                                     data-title="<?php $attachments->title(); ?>">
                                            <?php else: ?>
                                                <i class="mime-<?php echo $mime; ?> media-preview-icon"></i>
                                            <?php endif; ?>
                                        </td>
                                        <td>
                                            <i class="mime-<?php echo $mime; ?>"></i>
                                            <a href="<?php $options->adminUrl('media.php?cid=' . $attachments->cid); ?>"><?php $attachments->title(); ?></a>
                                            <?php if (!empty($fileUrl)): ?>
                                                <a href="<?php echo $fileUrl; ?>" target="_blank"
                                                   title="<?php _e('浏览 %s', $attachments->title); ?>"><i
                                                        class="i-exlink"></i></a>
                                            <?php endif; ?>
                                        </td>
                                        <td class="kit-hidden-mb"><?php $attachments->author(); ?></td>
                                        <td class="kit-hidden-mb">
                                            <?php if ($attachments->parentPost->cid): ?>
                                                <a href="<?php $options->adminUrl('write-' . (0 === strpos($attachments->parentPost->type, 'post') ? 'post' : 'page') . '.php?cid=' . $attachments->parentPost->cid); ?>"><?php $attachments->parentPost->title(); ?></a>
                                            <?php else: ?>
                                                <span class="description"><?php _e('未归档'); ?></span>
                                            <?php endif; ?>
                                        </td>
                                        <td><?php $attachments->dateWord(); ?></td>
                                    </tr>
                                <?php endwhile; ?>
                            <?php else: ?>
                                <tr>
                                    <td colspan="7"><h6 class="typecho-list-table-title"><?php _e('没有任何文件'); ?></h6>
                                    </td>
                                </tr>
                            <?php endif; ?>
                            </tbody>
                        </table><!-- end .typecho-list-table -->
                    </div><!-- end .typecho-table-wrap -->
                </form><!-- end .operate-form -->

                <div class="typecho-list-operate clearfix">
                    <form method="get">
                        <div class="operate">
                            <label><i class="sr-only"><?php _e('全选'); ?></i><input type="checkbox"
                                                                                   class="typecho-table-select-all"/></label>
                            <div class="btn-group btn-drop">
                                <button class="btn dropdown-toggle btn-s" type="button"><i
                                        class="sr-only"><?php _e('操作'); ?></i><?php _e('选中项'); ?> <i
                                        class="i-caret-down"></i></button>
                                <ul class="dropdown-menu">
                                    <li><a lang="<?php _e('你确认要删除这些文件吗?'); ?>"
                                           href="<?php $security->index('/action/contents-attachment-edit?do=delete'); ?>"><?php _e('删除'); ?></a>
                                    </li>
                                </ul>
                            </div>
                            <button class="btn btn-s btn-warn btn-operate"
                                    href="<?php $security->index('/action/contents-attachment-edit?do=clear'); ?>"
                                    lang="<?php _e('您确认要清理未归档的文件吗?'); ?>"><?php _e('清理未归档文件'); ?></button>
                        </div>
                        <?php if ($attachments->have()): ?>
                            <ul class="typecho-pager">
                                <?php $attachments->pageNav(); ?>
                            </ul>
                        <?php endif; ?>
                    </form>
                </div><!-- end .typecho-list-operate -->

            </div>
        </div><!-- end .typecho-page-main -->
    </div>
</div>

<!-- 图片预览模态框 -->
<div id="imageModal" class="modal">
    <span class="close">&times;</span>
    <div class="modal-content">
        <img class="modal-image" id="modalImage" src="" alt="">
        <div class="modal-title" id="modalTitle"></div>
    </div>
</div>

<script src="<?php $options->adminStaticUrl('js', 'moxie.js'); ?>"></script>
<script src="<?php $options->adminStaticUrl('js', 'plupload.js'); ?>"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 获取元素
    const toggleUploadBtn = document.getElementById('toggleUploadBtn');
    const cancelUploadBtn = document.getElementById('cancelUploadBtn');
    const uploadSection = document.getElementById('uploadSection');
    const uploadButton = document.getElementById('uploadButton');
    
    const browseLink = document.getElementById('browseLink');
    const fileInput = document.getElementById('fileInput');
    const uploadArea = document.getElementById('uploadArea');
    const uploadProgress = document.getElementById('uploadProgress');
    const progressFill = document.getElementById('progressFill');
    const progressText = document.getElementById('progressText');
    
    // 模态框元素
    const modal = document.getElementById('imageModal');
    const modalImg = document.getElementById('modalImage');
    const modalTitle = document.getElementById('modalTitle');
    const closeBtn = document.querySelector('.close');
    
    // 切换上传区域显示/隐藏
    // toggleUploadBtn.addEventListener('click', function() {
    //     uploadSection.classList.toggle('show');
    //     // 滚动到上传区域
    //     if (uploadSection.classList.contains('show')) {
    //         uploadSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    //     }
    // });
    
    // 切换上传区域显示/隐藏
    toggleUploadBtn.addEventListener('click', function() {
        uploadSection.classList.add('show');
        // 显示取消按钮,隐藏上传按钮
        toggleUploadBtn.style.display = 'none';
        cancelUploadBtn.style.display = 'inline-block';
        // 滚动到上传区域
        uploadSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    });
    // 取消上传
    cancelUploadBtn.addEventListener('click', function() {
        uploadSection.classList.remove('show');
        // 显示上传按钮,隐藏取消按钮
        toggleUploadBtn.style.display = 'inline-block';
        cancelUploadBtn.style.display = 'none';
    });


    
    // 点击链接触发文件选择
    uploadButton.addEventListener('click', function(e) {
        e.preventDefault();
        // 触发隐藏的文件输入框
        fileInput.click();
    });
    
    // 点击浏览链接也触发文件选择
    browseLink.addEventListener('click', function() {
        fileInput.click();
    });
    
    // 初始化上传器
    var uploader = new plupload.Uploader({
        runtimes: 'html5,flash,html4',
        browse_button: 'fileInput', // 绑定到隐藏的文件输入框
        container: 'uploadArea',
        drop_element: 'uploadArea',
        url: '<?php $security->index('/action/upload'); ?>',
        flash_swf_url: '<?php $options->adminStaticUrl('js', 'Moxie.swf'); ?>',
        filters: {
            max_file_size: '<?php echo $phpMaxFilesize ?>',
            mime_types: [{'title': '<?php _e('允许上传的文件'); ?>', 'extensions': '<?php echo implode(',', $options->allowedAttachmentTypes); ?>'}]
        },
        init: {
            PostInit: function() {
                console.log('Uploader initialized');
            },
            
            FilesAdded: function(up, files) {
                console.log('Files added:', files);
                // 显示进度条
                uploadProgress.style.display = 'block';
                
                // 开始上传
                up.start();
            },
            
            UploadProgress: function(up, file) {
                // 更新进度条
                var percent = file.percent;
                progressFill.style.width = percent + '%';
                progressText.textContent = percent + '%';
            },
            
            FileUploaded: function(up, file, response) {
                console.log('File uploaded:', response);
                if (response.status === 200) {
                    try {
                        var result = JSON.parse(response.response);
                        if (result && result[1]) {
                            // 上传成功,刷新页面显示新文件
                            location.reload();
                        }
                    } catch (e) {
                        console.error('Parse error:', e);
                    }
                }
                
                // 隐藏进度条
                setTimeout(function() {
                    uploadProgress.style.display = 'none';
                    progressFill.style.width = '0%';
                    progressText.textContent = '0%';
                    // 隐藏上传区域
                    uploadSection.classList.remove('show');
                }, 1000);
            },
            
            Error: function(up, err) {
                console.error('Upload error:', err);
                alert('上传出错: ' + err.message);
                
                // 隐藏进度条
                uploadProgress.style.display = 'none';
            }
        }
    });
    
    uploader.init();
    
    // 拖拽事件
    uploadArea.addEventListener('dragover', function(e) {
        e.preventDefault();
        this.classList.add('dragover');
    });
    
    uploadArea.addEventListener('dragleave', function(e) {
        e.preventDefault();
        this.classList.remove('dragover');
    });
    
    uploadArea.addEventListener('drop', function(e) {
        e.preventDefault();
        this.classList.remove('dragover');
    });
    
    // 图片预览功能
    const previewImages = document.querySelectorAll('.media-preview-img');
    previewImages.forEach(img => {
        img.addEventListener('click', function(e) {
            e.preventDefault();
            e.stopPropagation();
            
            const fullSrc = this.getAttribute('data-full-src');
            const title = this.getAttribute('data-title');
            
            if (fullSrc) {
                modalImg.src = fullSrc;
                modalTitle.textContent = title || '';
                modal.style.display = 'block';
                document.body.style.overflow = 'hidden';
            }
        });
    });
    
    // 关闭模态框
    function closeModal() {
        modal.style.display = 'none';
        document.body.style.overflow = '';
    }
    
    closeBtn.addEventListener('click', closeModal);
    
    modal.addEventListener('click', function(e) {
        if (e.target === modal) {
            closeModal();
        }
    });
    
    document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape' && modal.style.display === 'block') {
            closeModal();
        }
    });
});
</script>

<?php
include 'copyright.php';
include 'common-js.php';
include 'table-js.php';
include 'footer.php';
?>
]]>
0 https://blog.iletter.top/archives/447/#comments https://blog.iletter.top/feed/tag/PHP/
typecho博客Hello world插件增强 https://blog.iletter.top/archives/424/ https://blog.iletter.top/archives/424/ Thu, 07 Aug 2025 13:59:32 +0800 DelLevin 后台点击用户名称旁边的hello word会出现提示。

企业微信截图_17545462641024.png#B #S #R #60%

直接将该代码替换掉原本的代码即可:

<?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>';
    }
}
]]>
0 https://blog.iletter.top/archives/424/#comments https://blog.iletter.top/feed/tag/PHP/
typecho给后台admin界面添加图表分析 https://blog.iletter.top/archives/406/ https://blog.iletter.top/archives/406/ Sun, 27 Jul 2025 02:32:00 +0800 DelLevin 觉得后台admin空荡荡的,索性加个图表自己魔改一下下。以下是完整的index.php

网站概要-白荼日记-Powered-by-Typecho.png#B #S #R #60%

感兴趣的可以直接下载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" /]

]]>
0 https://blog.iletter.top/archives/406/#comments https://blog.iletter.top/feed/tag/PHP/
为typecho博客添加Gotify插件通知 https://blog.iletter.top/archives/405/ https://blog.iletter.top/archives/405/ Sun, 27 Jul 2025 02:05:00 +0800 DelLevin 受限于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);
        }
    }
}
]]>
2 https://blog.iletter.top/archives/405/#comments https://blog.iletter.top/feed/tag/PHP/