├── assets ├── _ttdf │ ├── ajax.js │ ├── motyf.css │ └── motyf.js ├── app.css ├── main.js └── welcome.css ├── .gitignore ├── app ├── components │ ├── AppFooter.php │ ├── AppHeader.php │ └── WelCome.php ├── app.config.php ├── fields.php └── setup.php ├── .idea ├── vcs.xml ├── .gitignore ├── modules.xml ├── TTDF.iml └── php.xml ├── index.php ├── core ├── Modules │ ├── Rest │ │ ├── Enums.php │ │ ├── DebugLogger.php │ │ ├── Middleware.php │ │ ├── Router.php │ │ └── Core.php │ ├── CacheManager.php │ ├── RouterAuto.php │ ├── Api.php │ ├── UseSeo.php │ └── Options.php ├── Widget │ ├── AddRoute.php │ ├── OOP │ │ ├── Comment.php │ │ ├── Site.php │ │ ├── Theme.php │ │ ├── User.php │ │ └── Post.php │ ├── TyAjax.php │ ├── Tools.php │ └── Hook.php ├── Main.php └── Static │ └── Options.css ├── 404.php ├── README.md ├── functions.php └── LICENSE /assets/_ttdf/ajax.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .idea/ 5 | .trae/ 6 | .repomap/ 7 | .virtualme/ -------------------------------------------------------------------------------- /app/components/AppFooter.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/app.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * 自定义css样式 5 | * 当然也可以创建一个custom.css文件 6 | * 然后在app.css中引入 7 | * @import "./custom.css"; 8 | */ 9 | 10 | @import "./welcome.css"; -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/TTDF.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
-------------------------------------------------------------------------------- /core/Modules/Rest/Enums.php: -------------------------------------------------------------------------------- 1 | '出错啦', 7 | 'description' => '您访问的页面不存在', 8 | 'keywords' => '404, error, 错误' 9 | ]; 10 | 11 | // 确保 Archive 部件已初始化 12 | $archive = Typecho_Widget::widget('Widget_Archive', array('type' => 'error')); 13 | 14 | Get::Components('AppHeader'); 15 | ?> 16 |
17 |
18 | 你似乎来到了没有知识存在的荒原 19 |
20 |
21 | The Intuitive Typecho Theme Framework. 4 | 5 | ✨ **核心特性** 6 | 7 | - 🧩 组件化 8 | - 🛣️ 自动路由 9 | - 🔧 配置管理 10 | - 🔌 内置API支持 11 | 12 | **运行环境** 13 | 14 | PHP 8.1+ / Typecho 1.2+ 15 | 16 | 🚧 **注意** 17 | 18 | 当前为开发版本,生产环境请使用[稳定版](https://github.com/YuiNijika/TTDF/releases) 19 | 20 | ## 快速入门 21 | 22 | [开发指南](https://typecho.dev/) | [集成Vite&Vue](https://github.com/YuiNijika/TTDF-Vite) 23 | 24 | ### 功能亮点 25 | 26 | - 轻量核心 27 | - Ajax支持 28 | - REST API 29 | - 事件系统 30 | - 路由管理 31 | - 配置框架 32 | - 快捷调用 33 | - 数据查询 34 | - 数据库操作 35 | - 主题设置表 36 | -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 13 | 14 | 16 | 17 | 19 | 20 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 鼠子Tomoriゞ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /core/Widget/AddRoute.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'debug' => false, // 调试模式 11 | 'compress_html' => false, // HTML压缩 12 | 'fields' => [ 13 | 'enabled' => false, // 自定义字段插件 14 | ], 15 | 'assets' => [ 16 | 'dir' => 'assets/', // 本地资源目录 17 | 'cdn' => [ 18 | 'enabled' => false, // 是否启用CDN 19 | 'url' => Helper::options()->themeUrl . '/assets/', // CDN URL 20 | ] 21 | ], 22 | ], 23 | 24 | 'plugins' => [ 25 | 'tyajax' => [ 26 | 'enabled' => false, // TyAjax模块 27 | ], 28 | ], 29 | 30 | // 模块配置 31 | 'modules' => [ 32 | 'gravatar' => [ 33 | 'prefix' => 'https://cravatar.cn/avatar/', // Gravatar前缀 34 | ], 35 | 'restapi' => [ 36 | 'enabled' => false, // 是否启用REST API 37 | 'route' => 'ty-json', // REST API路由 38 | 'override_setting' => 'RESTAPI_Switch', // 主题设置项名称 39 | 'token' => [ 40 | 'enabled' => false, // 是否启用Token 41 | 'value' => '1778273540', // Token值 42 | ], 43 | 'limit' => [ 44 | 'get' => 'attachments', // 禁止GET请求类 45 | 'post' => 'comments', // 禁止POST请求类 46 | ], 47 | 'headers' => [ 48 | 'access_control_allow_origin' => '*', // 跨域配置 49 | 'access_control_allow_methods' => 'GET,POST', // 允许的请求方法 50 | ], 51 | ], 52 | ], 53 | ]; -------------------------------------------------------------------------------- /core/Modules/Rest/DebugLogger.php: -------------------------------------------------------------------------------- 1 | 'Text', 8 | 'name' => 'TTDF_Fields_Text', 9 | 'value' => '', // 默认值为空字符串 10 | 'label' => '文本框', 11 | 'description' => '这是一个文本框~', 12 | ], 13 | [ 14 | // Textarea 15 | 'type' => 'Textarea', 16 | 'name' => 'TTDF_Fields_Textarea', 17 | 'value' => '', // 默认值为空字符串 18 | 'label' => '文本域', 19 | 'description' => '这是一个文本域~', 20 | // 设置字段属性 21 | 'attributes' => [ 22 | 'style' => 'height: 100px;' 23 | ] 24 | ], 25 | [ 26 | // Radio 27 | 'type' => 'Radio', 28 | 'name' => 'TTDF_Fields_Radio', 29 | 'value' => '', // 默认值为空字符串 30 | 'label' => '单选框', 31 | 'description' => '这是一个单选框~', 32 | 'options' => [ 33 | 'option1' => '选项一', 34 | 'option2' => '选项二', 35 | 'option3' => '选项三' 36 | ] 37 | ], 38 | [ 39 | // Select 40 | 'type' => 'Select', 41 | 'name' => 'TTDF_Fields_Select', 42 | 'value' => '', // 默认值为空字符串 43 | 'label' => '下拉框', 44 | 'description' => '这是一个下拉框~', 45 | 'options' => [ 46 | 'option1' => '选项一', 47 | 'option2' => '选项二', 48 | 'option3' => '选项三' 49 | ] 50 | ], 51 | [ 52 | // Checkbox 53 | 'type' => 'Checkbox', 54 | 'name' => 'TTDF_Fields_Checkbox', 55 | 'value' => [], // 默认值为空数组 56 | 'label' => '多选框', 57 | 'description' => '这是一个多选框~', 58 | 'options' => [ 59 | 'option1' => '选项一', 60 | 'option2' => '选项二', 61 | 'option3' => '选项三' 62 | ] 63 | ] 64 | ]; 65 | -------------------------------------------------------------------------------- /core/Modules/Rest/Middleware.php: -------------------------------------------------------------------------------- 1 | setStatus($code->value); 47 | header('Content-Type: application/json; charset=UTF-8'); 48 | } 49 | 50 | echo json_encode([ 51 | 'code' => $code->value, 52 | 'message' => $message, 53 | 'timestamp' => time() 54 | ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 55 | exit; 56 | } 57 | } -------------------------------------------------------------------------------- /app/components/WelCome.php: -------------------------------------------------------------------------------- 1 | 4 |
5 | 23 |
24 |
-------------------------------------------------------------------------------- /core/Widget/OOP/Comment.php: -------------------------------------------------------------------------------- 1 | comments; 22 | } catch (Exception $e) { 23 | self::handleError('获取评论失败', $e); 24 | } 25 | } 26 | 27 | // 获取评论页面 28 | public static function CommentsPage() 29 | { 30 | try { 31 | echo self::getArchive()->commentsPage; 32 | } catch (Exception $e) { 33 | self::handleError('获取评论页面失败', $e); 34 | } 35 | } 36 | 37 | // 获取评论列表 38 | public static function CommentsList() 39 | { 40 | try { 41 | echo self::getArchive()->commentsList; 42 | } catch (Exception $e) { 43 | self::handleError('获取评论列表失败', $e); 44 | } 45 | } 46 | 47 | // 获取评论数 48 | public static function CommentsNum() 49 | { 50 | try { 51 | echo self::getArchive()->commentsNum; 52 | } catch (Exception $e) { 53 | self::handleError('获取评论数失败', $e); 54 | } 55 | } 56 | 57 | // 获取评论id 58 | public static function RespondId() 59 | { 60 | try { 61 | echo self::getArchive()->respondId; 62 | } catch (Exception $e) { 63 | self::handleError('获取评论id失败', $e); 64 | } 65 | } 66 | 67 | // 取消回复 68 | public static function CancelReply() 69 | { 70 | try { 71 | echo self::getArchive()->cancelReply(); 72 | } catch (Exception $e) { 73 | self::handleError('取消回复失败', $e); 74 | } 75 | } 76 | 77 | // Remember 78 | public static function Remember($field) 79 | { 80 | try { 81 | echo self::getArchive()->remember($field); 82 | } catch (Exception $e) { 83 | self::handleError('获取Remember失败', $e); 84 | } 85 | } 86 | 87 | // 获取评论表单 88 | public static function CommentsForm() 89 | { 90 | try { 91 | echo self::getArchive()->commentsForm; 92 | } catch (Exception $e) { 93 | self::handleError('获取评论表单失败', $e); 94 | } 95 | } 96 | 97 | // 获取分页 98 | public static function PageNav($prev = '« 前一页', $next = '后一页 »') 99 | { 100 | try { 101 | // 使用评论专用的 Widget 102 | $comments = \Widget_Comments_Archive::widget('Widget_Comments_Archive'); 103 | $comments->pageNav($prev, $next); 104 | } catch (Exception $e) { 105 | self::handleError('评论分页导航失败', $e); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /core/Widget/OOP/Site.php: -------------------------------------------------------------------------------- 1 | themeUrl; 33 | } else if ($echo && isset($theme)) { 34 | echo \Helper::options()->themeUrl($path, $theme); 35 | } 36 | 37 | \Helper::options()->themeUrl($path, $theme); 38 | } catch (Exception $e) { 39 | return self::handleError('获取主题URL失败', $e); 40 | } 41 | } 42 | 43 | /** 44 | * 获取主题的绝对路径(末尾不带 / ) 45 | * 46 | * @param bool|null $echo 当设置为 true 时,会直接输出; 47 | * 当设置为 false 时,则返回结果径。 48 | * @return string|null 49 | * @throws Exception 50 | */ 51 | public static function Dir(?bool $echo = true) 52 | { 53 | try { 54 | $Dir = self::getArchive()->getThemeDir(); 55 | 56 | if ($echo) echo $Dir; 57 | 58 | return $Dir; 59 | } catch (Exception $e) { 60 | return self::handleError('获取主题绝对路径失败', $e); 61 | } 62 | } 63 | 64 | /** 65 | * 定义AssetsUrl 66 | * 防止之前写的主题失效 67 | */ 68 | public static function AssetsUrl() 69 | { 70 | return self::Url(false, 'Assets'); 71 | } 72 | 73 | /** 74 | * 获取主题名称 75 | * 76 | * @param bool|null $echo 当设置为 true 时,会直接输出; 77 | * 当设置为 false 时,则返回结果。 78 | * @return string|null 79 | * @throws Exception 80 | */ 81 | public static function Name(?bool $echo = true) 82 | { 83 | try { 84 | $Name = \Helper::options()->theme; 85 | 86 | if ($echo) echo $Name; 87 | 88 | return $Name; 89 | } catch (Exception $e) { 90 | return self::handleError('获取主题名称失败', $e); 91 | } 92 | } 93 | 94 | /** 95 | * 获取主题作者 96 | * 97 | * @param bool|null $echo 当设置为 true 时,会直接输出; 98 | * 当设置为 false 时,则返回结果。 99 | * @return string|null 100 | * @throws Exception 101 | */ 102 | public static function Author(?bool $echo = true) 103 | { 104 | try { 105 | $infoFile = dirname(__DIR__, 3) . '/index.php'; // 主题根目录的 index.php 106 | 107 | if (!file_exists($infoFile)) { 108 | throw new Exception("主题信息文件不存在: {$infoFile}"); 109 | } 110 | 111 | $author = \Typecho\Plugin::parseInfo($infoFile); 112 | 113 | if (empty($author['author'])) { 114 | $author['author'] = null; 115 | } 116 | 117 | if ($echo) echo $author['author']; 118 | 119 | return $author['author']; 120 | } catch (Exception $e) { 121 | return self::handleError('获取主题作者失败', $e); 122 | } 123 | } 124 | 125 | /** 126 | * 获取主题版本 127 | * 128 | * @param bool|null $echo 当设置为 true 时,会直接输出; 129 | * 当设置为 false 时,则返回结果。 130 | * @return string|null 131 | * @throws Exception 132 | */ 133 | public static function Ver(?bool $echo = true) 134 | { 135 | try { 136 | $infoFile = dirname(__DIR__, 3) . '/index.php'; // 主题根目录的 index.php 137 | 138 | if (!file_exists($infoFile)) { 139 | throw new Exception("主题信息文件不存在: {$infoFile}"); 140 | } 141 | 142 | $ver = \Typecho\Plugin::parseInfo($infoFile); 143 | 144 | if (empty($ver['version'])) { 145 | $ver['version'] = null; 146 | } 147 | 148 | if ($echo) { 149 | echo $ver['version']; 150 | } 151 | 152 | return $ver['version']; 153 | } catch (Exception $e) { 154 | return self::handleError('获取主题版本失败', $e); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /assets/_ttdf/motyf.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | /** 4 | * MotyfJS 5 | * 基于Notyf修改的轻量级消息提示组件 6 | * @author 鼠子Tomoriゞ 7 | * @link https://github.com/YuiNijika/MotynJS 8 | */ 9 | 10 | .motym { 11 | position: fixed; 12 | z-index: 10000000; 13 | width: 320px; 14 | } 15 | 16 | /* 默认位置 */ 17 | .motym:not(.top-left):not(.top-right):not(.bottom-left):not(.top-center):not(.bottom-center):not(.left-center):not(.right-center) { 18 | bottom: 20px; 19 | right: 20px; 20 | } 21 | 22 | /* 右下角 */ 23 | .motym.bottom-right { 24 | bottom: 20px; 25 | right: 20px; 26 | } 27 | 28 | /* 左下角 */ 29 | .motym.bottom-left { 30 | bottom: 20px; 31 | left: 20px; 32 | } 33 | 34 | /* 右上角 */ 35 | .motym.top-right { 36 | top: 20px; 37 | right: 20px; 38 | } 39 | 40 | /* 左上角 */ 41 | .motym.top-left { 42 | top: 20px; 43 | left: 20px; 44 | } 45 | 46 | /* 顶部居中 */ 47 | .motym.top-center { 48 | top: 20px; 49 | left: 50%; 50 | transform: translateX(-50%); 51 | width: auto; 52 | max-width: 100%; 53 | } 54 | 55 | /* 底部居中 */ 56 | .motym.bottom-center { 57 | bottom: 20px; 58 | left: 50%; 59 | transform: translateX(-50%); 60 | width: auto; 61 | max-width: 100%; 62 | } 63 | 64 | /* 左侧居中 */ 65 | .motym.left-center { 66 | left: 20px; 67 | top: 50%; 68 | transform: translateY(-50%); 69 | width: auto; 70 | max-width: 100%; 71 | } 72 | 73 | /* 右侧居中 */ 74 | .motym.right-center { 75 | right: 20px; 76 | top: 50%; 77 | transform: translateY(-50%); 78 | width: auto; 79 | max-width: 100%; 80 | } 81 | 82 | .motyf-message { 83 | margin-top: 10px; 84 | transition: all 0.3s ease; 85 | } 86 | 87 | /* 根据位置调整消息间距 */ 88 | .motym.top-left .motyf-message, 89 | .motym.top-right .motyf-message, 90 | .motym.top-center .motyf-message { 91 | margin-top: 0; 92 | margin-bottom: 10px; 93 | } 94 | 95 | /* 垂直居中位置的间距调整 */ 96 | .motym.left-center .motyf-message, 97 | .motym.right-center .motyf-message { 98 | margin: 10px 0; 99 | } 100 | 101 | /* 进入动画 */ 102 | .motyf-message.motyf-enter .motyf { 103 | animation: motyf-enter-right 0.4s cubic-bezier(0.35, 0.71, 0.46, 1.08); 104 | } 105 | 106 | /* 左侧位置的进入动画 */ 107 | .motym.bottom-left .motyf-message.motyf-enter .motyf, 108 | .motym.top-left .motyf-message.motyf-enter .motyf, 109 | .motym.left-center .motyf-message.motyf-enter .motyf { 110 | animation: motyf-enter-left 0.4s cubic-bezier(0.35, 0.71, 0.46, 1.08); 111 | } 112 | 113 | /* 顶部和底部居中位置的进入动画 */ 114 | .motym.top-center .motyf-message.motyf-enter .motyf, 115 | .motym.bottom-center .motyf-message.motyf-enter .motyf { 116 | animation: motyf-enter-down 0.4s cubic-bezier(0.35, 0.71, 0.46, 1.08); 117 | } 118 | 119 | /* 退出动画 */ 120 | .motyf-message.motym-out .motyf { 121 | opacity: 0; 122 | transform: translateX(100%); 123 | transition: all 0.3s ease; 124 | } 125 | 126 | /* 左侧位置的退出动画 */ 127 | .motym.bottom-left .motyf-message.motym-out .motyf, 128 | .motym.top-left .motyf-message.motym-out .motyf, 129 | .motym.left-center .motyf-message.motym-out .motyf { 130 | transform: translateX(-100%); 131 | } 132 | 133 | /* 顶部和底部居中位置的退出动画 */ 134 | .motym.top-center .motyf-message.motym-out .motyf, 135 | .motym.bottom-center .motyf-message.motym-out .motyf { 136 | transform: translateY(-50%) scale(0.8); 137 | opacity: 0; 138 | } 139 | 140 | /* 右侧居中位置的退出动画 */ 141 | .motym.right-center .motyf-message.motym-out .motyf { 142 | transform: translateY(-50%) translateX(100%); 143 | opacity: 0; 144 | } 145 | 146 | @keyframes motyf-enter-right { 147 | 0% { 148 | transform: translateX(100%); 149 | opacity: 0; 150 | } 151 | 100% { 152 | transform: translateX(0); 153 | opacity: 1; 154 | } 155 | } 156 | 157 | @keyframes motyf-enter-left { 158 | 0% { 159 | transform: translateX(-100%); 160 | opacity: 0; 161 | } 162 | 100% { 163 | transform: translateX(0); 164 | opacity: 1; 165 | } 166 | } 167 | 168 | @keyframes motyf-enter-down { 169 | 0% { 170 | transform: translateY(-50%) scale(0.8); 171 | opacity: 0; 172 | } 173 | 100% { 174 | transform: translateY(0) scale(1); 175 | opacity: 1; 176 | } 177 | } 178 | 179 | .motyf { 180 | color: #fff; 181 | min-width: 200px; 182 | padding: 16px 24px 16px 54px; 183 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 184 | transform: translateX(0); 185 | transition: 0.3s; 186 | -webkit-backdrop-filter: saturate(5) blur(20px); 187 | backdrop-filter: saturate(5) blur(20px); 188 | border-radius: 4px; 189 | position: relative; 190 | overflow: hidden; 191 | background: linear-gradient(90deg, rgba(15, 147, 249, 0.7), rgba(61, 189, 249, 0.8)); 192 | } 193 | 194 | .motyf-icon { 195 | position: absolute; 196 | left: 16px; 197 | top: 50%; 198 | transform: translateY(-50%); 199 | height: 20px; 200 | width: 20px; 201 | } 202 | 203 | .motyf.success { 204 | background: linear-gradient(90deg, rgba(15, 147, 249, 0.7), rgba(61, 189, 249, 0.8)); 205 | } 206 | 207 | .motyf.info { 208 | background: linear-gradient(90deg, rgba(58, 162, 54, 0.8), rgba(89, 247, 131, 0.8)); 209 | } 210 | 211 | .motyf.warning { 212 | background: linear-gradient(90deg, rgba(253, 170, 71, 0.7), rgba(247, 154, 13, 0.8)); 213 | } 214 | 215 | .motyf.danger, .motyf.error { 216 | background: linear-gradient(90deg, rgba(253, 69, 28, 0.7), rgba(251, 110, 75, 0.8)); 217 | } 218 | 219 | .motyf .motyf-text { 220 | display: block; 221 | } -------------------------------------------------------------------------------- /assets/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TTDF主JavaScript模块 3 | */ 4 | 5 | // 自定义JS模块配置 6 | const CUSTOM_JS_MODULES = [ 7 | // demo: 8 | // { path: './custom.js', condition: () => true } 9 | ]; 10 | 11 | class TTDF_JSManager { 12 | constructor() { 13 | this.config = this.loadConfig(); 14 | this.modules = []; 15 | this.version = this.config.version?.theme || Date.now(); 16 | } 17 | 18 | /** 19 | * 加载配置 20 | * @returns {Object} 配置对象 21 | */ 22 | loadConfig() { 23 | if (typeof window.frameWorkConfig !== 'undefined') { 24 | return window.frameWorkConfig; 25 | } 26 | 27 | console.warn('TTDF configuration not found, using default settings'); 28 | return { 29 | TyAjax: false, 30 | RESTAPI: { 31 | enabled: false, 32 | route: 'ty-json' 33 | }, 34 | version: { 35 | theme: '1.0.0' 36 | } 37 | }; 38 | } 39 | 40 | /** 41 | * 初始化 42 | */ 43 | async init() { 44 | try { 45 | await this.loadModules(); 46 | // this.logInitialization(); 47 | } catch (error) { 48 | console.error('Failed to initialize TTDF:', error); 49 | } 50 | } 51 | 52 | /** 53 | * 根据配置加载模块 54 | * @returns {Promise} 模块加载Promise 55 | */ 56 | async loadModules() { 57 | const modulePromises = []; 58 | 59 | // 根据配置加载相应模块 60 | if (this.config.TyAjax) { 61 | modulePromises.push(this.loadModule('./_ttdf/ajax.js')); 62 | modulePromises.push(this.loadModule('./_ttdf/motyf.js')); 63 | this.loadCSS('./_ttdf/motyf.css'); 64 | } 65 | 66 | // 加载自定义模块 67 | for (const customModule of CUSTOM_JS_MODULES) { 68 | try { 69 | // 如果没有条件或者条件满足,则加载模块 70 | if (!customModule.condition || customModule.condition()) { 71 | if (customModule.path.endsWith('.css')) { 72 | this.loadCSS(customModule.path); 73 | } else { 74 | modulePromises.push(this.loadModule(customModule.path)); 75 | } 76 | } 77 | } catch (error) { 78 | console.error(`Failed to process custom module: ${customModule.path}`, error); 79 | } 80 | } 81 | 82 | if (modulePromises.length > 0) { 83 | this.modules = await Promise.all(modulePromises); 84 | } 85 | } 86 | 87 | /** 88 | * 加载单个模块 89 | * @param {string} modulePath 模块路径 90 | * @returns {Promise} 模块导入Promise 91 | */ 92 | async loadModule(modulePath) { 93 | try { 94 | // 为模块路径添加版本号参数 95 | const versionedPath = this.addVersionToPath(modulePath); 96 | const module = await import(versionedPath); 97 | console.log(`Module loaded: ${modulePath}`); 98 | return module; 99 | } catch (error) { 100 | console.error(`Failed to load module: ${modulePath}`, error); 101 | throw error; 102 | } 103 | } 104 | 105 | /** 106 | * 动态加载CSS文件 107 | * @param {string} cssPath CSS文件路径 108 | */ 109 | loadCSS(cssPath) { 110 | // 为CSS路径添加版本号参数 111 | const versionedPath = this.addVersionToPath(cssPath); 112 | 113 | // 检查是否已经加载过该CSS文件 114 | const existingLink = document.querySelector(`link[href="${cssPath}"]`) || 115 | document.querySelector(`link[href="${versionedPath}"]`); 116 | 117 | if (existingLink) { 118 | console.log(`CSS already loaded: ${cssPath}`); 119 | return; 120 | } 121 | 122 | try { 123 | const link = document.createElement('link'); 124 | link.rel = 'stylesheet'; 125 | link.type = 'text/css'; 126 | link.href = versionedPath; 127 | link.onload = () => { 128 | console.log(`CSS loaded successfully: ${cssPath}`); 129 | }; 130 | link.onerror = (error) => { 131 | console.error(`Failed to load CSS: ${cssPath}`, error); 132 | }; 133 | 134 | document.head.appendChild(link); 135 | console.log(`Loading CSS: ${cssPath}`); 136 | } catch (error) { 137 | console.error(`Failed to create CSS link: ${cssPath}`, error); 138 | } 139 | } 140 | 141 | /** 142 | * 为路径添加版本号参数 143 | * @param {string} path 原始路径 144 | * @returns {string} 添加版本号后的路径 145 | */ 146 | addVersionToPath(path) { 147 | // 检查路径是否已经有查询参数 148 | const separator = path.includes('?') ? '&' : '?'; 149 | return `${path}${separator}v=${this.version}`; 150 | } 151 | 152 | /** 153 | * 记录初始化信息 154 | */ 155 | logInitialization() { 156 | console.log('All TTDF modules loaded successfully'); 157 | } 158 | 159 | /** 160 | * 获取配置值 161 | * @param {string} key 配置键 162 | * @param {*} defaultValue 默认值 163 | * @returns {*} 配置值 164 | */ 165 | getConfig(key, defaultValue = null) { 166 | return this.config[key] !== undefined ? this.config[key] : defaultValue; 167 | } 168 | 169 | /** 170 | * 检查是否启用了特定功能 171 | * @param {string} feature 功能名称 172 | * @returns {boolean} 是否启用 173 | */ 174 | isFeatureEnabled(feature) { 175 | return !!this.getConfig(feature, false); 176 | } 177 | } 178 | 179 | // 初始化 180 | function initTTDF() { 181 | const ttdfManager = new TTDF_JSManager(); 182 | ttdfManager.init(); 183 | } 184 | 185 | // 等待DOM加载完成后初始化 186 | document.addEventListener('DOMContentLoaded', initTTDF); -------------------------------------------------------------------------------- /core/Modules/CacheManager.php: -------------------------------------------------------------------------------- 1 | = self::MAX_CACHE_SIZE) { 49 | self::cleanup(); 50 | } 51 | 52 | self::$cache[$key] = $value; 53 | self::$expiry[$key] = time() + $ttl; 54 | 55 | return true; 56 | } catch (Exception $e) { 57 | if (self::$errorHandler) { 58 | self::$errorHandler->warning('缓存设置失败', ['key' => $key], $e); 59 | } 60 | return false; 61 | } 62 | } 63 | 64 | /** 65 | * 获取缓存 66 | * @param string $key 缓存键 67 | * @param mixed $default 默认值 68 | * @return mixed 69 | */ 70 | public static function get(string $key, $default = null) 71 | { 72 | try { 73 | self::init(); 74 | 75 | // 检查缓存是否存在 76 | if (!isset(self::$cache[$key])) { 77 | return $default; 78 | } 79 | 80 | // 检查是否过期 81 | if (isset(self::$expiry[$key]) && time() > self::$expiry[$key]) { 82 | self::delete($key); 83 | return $default; 84 | } 85 | 86 | return self::$cache[$key]; 87 | } catch (Exception $e) { 88 | if (self::$errorHandler) { 89 | self::$errorHandler->warning('缓存获取失败', ['key' => $key], $e); 90 | } 91 | return $default; 92 | } 93 | } 94 | 95 | /** 96 | * 检查缓存是否存在且未过期 97 | * @param string $key 缓存键 98 | * @return bool 99 | */ 100 | public static function has(string $key): bool 101 | { 102 | if (!isset(self::$cache[$key])) { 103 | return false; 104 | } 105 | 106 | // 检查是否过期 107 | if (isset(self::$expiry[$key]) && time() > self::$expiry[$key]) { 108 | self::delete($key); 109 | return false; 110 | } 111 | 112 | return true; 113 | } 114 | 115 | /** 116 | * 删除缓存 117 | * @param string $key 缓存键 118 | * @return bool 119 | */ 120 | public static function delete(string $key): bool 121 | { 122 | unset(self::$cache[$key], self::$expiry[$key]); 123 | return true; 124 | } 125 | 126 | /** 127 | * 清空所有缓存 128 | * @return bool 129 | */ 130 | public static function clear(): bool 131 | { 132 | self::$cache = []; 133 | self::$expiry = []; 134 | return true; 135 | } 136 | 137 | /** 138 | * 清理过期缓存 139 | * @return int 清理的条目数 140 | */ 141 | public static function cleanup(): int 142 | { 143 | $cleaned = 0; 144 | $now = time(); 145 | 146 | foreach (self::$expiry as $key => $expiry) { 147 | if ($now > $expiry) { 148 | unset(self::$cache[$key], self::$expiry[$key]); 149 | $cleaned++; 150 | } 151 | } 152 | 153 | // 如果清理后仍然超过限制,删除最老的条目 154 | if (count(self::$cache) >= self::MAX_CACHE_SIZE) { 155 | $toRemove = count(self::$cache) - self::MAX_CACHE_SIZE + 100; // 多删除一些 156 | $keys = array_keys(self::$cache); 157 | for ($i = 0; $i < $toRemove && $i < count($keys); $i++) { 158 | $key = $keys[$i]; 159 | unset(self::$cache[$key], self::$expiry[$key]); 160 | $cleaned++; 161 | } 162 | } 163 | 164 | return $cleaned; 165 | } 166 | 167 | /** 168 | * 获取缓存统计信息 169 | * @return array 170 | */ 171 | public static function getStats(): array 172 | { 173 | $now = time(); 174 | $expired = 0; 175 | 176 | foreach (self::$expiry as $expiry) { 177 | if ($now > $expiry) { 178 | $expired++; 179 | } 180 | } 181 | 182 | return [ 183 | 'total' => count(self::$cache), 184 | 'expired' => $expired, 185 | 'active' => count(self::$cache) - $expired, 186 | 'memory_usage' => memory_get_usage(true) 187 | ]; 188 | } 189 | 190 | /** 191 | * 生成缓存键 192 | * @param string $prefix 前缀 193 | * @param array $params 参数 194 | * @return string 195 | */ 196 | public static function generateKey(string $prefix, array $params = []): string 197 | { 198 | if (empty($params)) { 199 | return $prefix; 200 | } 201 | 202 | return $prefix . '_' . md5(serialize($params)); 203 | } 204 | } -------------------------------------------------------------------------------- /core/Modules/RouterAuto.php: -------------------------------------------------------------------------------- 1 | setStatus(200); 32 | self::renderMatchedFile($matchedFile, $path); 33 | exit; // 匹配到路由时终止执行 34 | } 35 | 36 | // 未匹配到路由时不作任何处理,让Typecho继续 37 | } 38 | 39 | private static function getRequestPath() 40 | { 41 | $requestUri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); 42 | return trim($requestUri, '/'); 43 | } 44 | 45 | // 检查是否应该跳过路由处理 46 | private static function shouldSkipRoute($path) 47 | { 48 | // 不处理以 feed 开头的路径 49 | if (strpos($path, 'feed') === 0) { 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | private static function findMatchingFile($requestPath) 56 | { 57 | $requestParts = $requestPath ? explode('/', $requestPath) : []; 58 | return self::scanDirectory(self::$pagesDir, $requestParts); 59 | } 60 | 61 | private static function scanDirectory($currentDir, $remainingParts, $index = 0) 62 | { 63 | // 确保当前路径是目录 64 | if (!is_dir($currentDir)) { 65 | return null; 66 | } 67 | 68 | $files = scandir($currentDir); 69 | if ($files === false) { 70 | return null; // scandir 失败 71 | } 72 | 73 | // 如果是最后一部分,优先检查精确匹配 74 | if ($index >= count($remainingParts)) { 75 | $exactFile = $currentDir . '/index.php'; 76 | if (file_exists($exactFile)) { 77 | return [ 78 | 'file' => $exactFile, 79 | 'params' => [] 80 | ]; 81 | } 82 | return null; 83 | } 84 | 85 | $currentPart = $remainingParts[$index]; 86 | 87 | // 检查精确匹配的目录/文件 88 | if (in_array($currentPart . '.php', $files)) { 89 | $filePath = $currentDir . '/' . $currentPart . '.php'; 90 | if ($index === count($remainingParts) - 1) { 91 | return [ 92 | 'file' => $filePath, 93 | 'params' => [] 94 | ]; 95 | } 96 | } 97 | 98 | if (is_dir($currentDir . '/' . $currentPart)) { 99 | $result = self::scanDirectory( 100 | $currentDir . '/' . $currentPart, 101 | $remainingParts, 102 | $index + 1 103 | ); 104 | if ($result) return $result; 105 | } 106 | 107 | // 检查动态路由 [param].php 108 | foreach ($files as $file) { 109 | if (preg_match('/^\[(\w+)\]\.php$/', $file, $matches)) { 110 | $paramName = $matches[1]; 111 | if ($index === count($remainingParts) - 1) { 112 | return [ 113 | 'file' => $currentDir . '/' . $file, 114 | 'params' => [$paramName => $currentPart] 115 | ]; 116 | } 117 | 118 | // 确保 $currentDir . '/' . $file 是目录才继续递归 119 | $nextDir = $currentDir . '/' . $file; 120 | if (is_dir($nextDir)) { 121 | $result = self::scanDirectory( 122 | $nextDir, 123 | $remainingParts, 124 | $index + 1 125 | ); 126 | if ($result) { 127 | $result['params'][$paramName] = $currentPart; 128 | return $result; 129 | } 130 | } 131 | } 132 | } 133 | 134 | // 检查可选动态路由 [[param]].php 135 | foreach ($files as $file) { 136 | if (preg_match('/^\[\[(\w+)\]\]\.php$/', $file, $matches)) { 137 | $paramName = $matches[1]; 138 | $nextDir = $currentDir . '/' . $file; 139 | 140 | // 确保 $nextDir 是目录才继续递归 141 | if (is_dir($nextDir)) { 142 | $result = self::scanDirectory( 143 | $nextDir, 144 | $remainingParts, 145 | $index + 1 146 | ); 147 | if ($result) { 148 | if ($index < count($remainingParts)) { 149 | $result['params'][$paramName] = $currentPart; 150 | } 151 | return $result; 152 | } 153 | } 154 | 155 | // 或者作为终止文件 156 | if ($index === count($remainingParts) - 1) { 157 | return [ 158 | 'file' => $currentDir . '/' . $file, 159 | 'params' => [$paramName => $currentPart] 160 | ]; 161 | } 162 | } 163 | } 164 | 165 | return null; 166 | } 167 | 168 | private static function renderMatchedFile($match, $path) 169 | { 170 | // 设置参数到$_GET 171 | foreach ($match['params'] as $key => $value) { 172 | $_GET[$key] = $value; 173 | } 174 | 175 | // 设置路由信息 176 | $GLOBALS['_ttdf_route'] = [ 177 | 'path' => $path, 178 | 'params' => $match['params'], 179 | 'file' => str_replace(self::$pagesDir, '', $match['file']) 180 | ]; 181 | 182 | include $match['file']; 183 | } 184 | } 185 | 186 | TTDF_AutoRouter::init(); -------------------------------------------------------------------------------- /core/Modules/Api.php: -------------------------------------------------------------------------------- 1 | getDisplayErrors(); 68 | $errorHandler->setDisplayErrors(false); 69 | } 70 | } 71 | 72 | try { 73 | // 记录API请求开始 74 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 75 | TTDF_Debug::logApiProcess('START', [ 76 | 'request_uri' => $requestUri, 77 | 'base_path' => $basePath, 78 | 'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN' 79 | ]); 80 | } 81 | 82 | // 验证Token 83 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 84 | TTDF_Debug::logApiProcess('TOKEN_VALIDATION'); 85 | } 86 | TokenValidator::validate(); 87 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 88 | TTDF_Debug::logApiProcess('TOKEN_VALIDATION_SUCCESS'); 89 | } 90 | 91 | // 初始化组件 92 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 93 | TTDF_Debug::logApiProcess('INITIALIZING_COMPONENTS'); 94 | } 95 | $request = new ApiRequest(); 96 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 97 | TTDF_Debug::logApiProcess('API_REQUEST_CREATED', [ 98 | 'path' => $request->path, 99 | 'path_parts' => $request->pathParts 100 | ]); 101 | } 102 | 103 | $response = new ApiResponse($request->contentFormat); 104 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 105 | TTDF_Debug::logApiProcess('API_RESPONSE_CREATED'); 106 | } 107 | 108 | $db = new TTDF_Db_API(); 109 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 110 | TTDF_Debug::logApiProcess('TTDF_Db_API_CREATED'); 111 | } 112 | 113 | $formatter = new ApiFormatter($db, $request->contentFormat, $request->excerptLength); 114 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 115 | TTDF_Debug::logApiProcess('API_FORMATTER_CREATED'); 116 | } 117 | 118 | $api = new TTDF_API($request, $response, $db, $formatter); 119 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 120 | TTDF_Debug::logApiProcess('TTDF_API_CREATED'); 121 | } 122 | 123 | // 处理请求 124 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 125 | TTDF_Debug::logApiProcess('HANDLING_REQUEST'); 126 | } 127 | $api->handleRequest(); 128 | } catch (Throwable $e) { 129 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 130 | TTDF_Debug::logApiError('API Bootstrap Error', $e); 131 | } 132 | 133 | if (!headers_sent()) { 134 | http_response_code(500); 135 | header('Content-Type: application/json; charset=UTF-8'); 136 | } 137 | error_log("API Bootstrap Error: " . $e->getMessage()); 138 | echo json_encode([ 139 | 'code' => 500, 140 | 'message' => 'API failed to start.', 141 | 'error' => defined('__TYPECHO_DEBUG__') && __TYPECHO_DEBUG__ ? $e->getMessage() : 'An unexpected error occurred.' 142 | ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 143 | exit; 144 | } finally { 145 | // 恢复原始错误处理设置 146 | if (!(TTDF_CONFIG['DEBUG'] ?? false)) { 147 | if ($originalDisplayErrors !== null) { 148 | ini_set('display_errors', $originalDisplayErrors); 149 | } 150 | 151 | if ($originalErrorHandlerDisplay !== null && class_exists('TTDF_ErrorHandler')) { 152 | $errorHandler = TTDF_ErrorHandler::getInstance(); 153 | $errorHandler->setDisplayErrors($originalErrorHandlerDisplay); 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /core/Modules/UseSeo.php: -------------------------------------------------------------------------------- 1 | title; 22 | } catch (Exception $e) { 23 | self::handleError("获取标题失败", $e); 24 | } 25 | } 26 | 27 | // 获取分类 28 | public static function Category( 29 | $split = ",", 30 | $link = false, 31 | $default = "暂无分类" 32 | ) { 33 | try { 34 | echo self::getArchive()->category($split, $link, $default); 35 | } catch (Exception $e) { 36 | self::handleError("获取分类失败", $e); 37 | echo $default; 38 | } 39 | } 40 | 41 | // 获取标签 42 | public static function Tags( 43 | $split = ",", 44 | $link = false, 45 | $default = "暂无标签" 46 | ) { 47 | try { 48 | echo self::getArchive()->tags($split, $link, $default); 49 | } catch (Exception $e) { 50 | self::handleError("获取标签失败", $e); 51 | echo $default; 52 | } 53 | } 54 | 55 | // 获取摘要 56 | public static function Excerpt($length = 0) 57 | { 58 | try { 59 | $excerpt = strip_tags(self::getArchive()->excerpt); // 去除 HTML 标签 60 | if ($length > 0) { 61 | $excerpt = mb_substr($excerpt, 0, $length, "UTF-8"); 62 | } 63 | return $excerpt; 64 | } catch (Exception $e) { 65 | self::handleError("获取摘要失败", $e); 66 | return ""; 67 | } 68 | } 69 | } 70 | function TTDF_SEO_Title() 71 | { 72 | 73 | if (defined("useSeo")) { 74 | echo useSeo["title"]; 75 | return; 76 | } 77 | if (class_exists("useSeo")) { 78 | useSeo::Title(); 79 | return; 80 | } 81 | 82 | $archiveTitle = GetPost::ArchiveTitle( 83 | [ 84 | "category" => _t("%s 分类"), 85 | "search" => _t("搜索结果"), 86 | "tag" => _t("%s 标签"), 87 | "author" => _t("%s 的空间"), 88 | ], 89 | "", 90 | " - " 91 | ); 92 | echo $archiveTitle; 93 | if ( 94 | Get::Is("index") && 95 | !empty(Get::Options("SubTitle")) && 96 | Get::CurrentPage() > 1 97 | ) { 98 | echo "第" . Get::CurrentPage() . "页 - "; 99 | } 100 | $title = Get::Options("title"); 101 | echo $title; 102 | if (Get::Is("index") && !empty(Get::Options("SubTitle"))) { 103 | echo " - "; 104 | $SubTitle = Get::Options("SubTitle"); 105 | echo $SubTitle; 106 | } 107 | } 108 | function TTDF_SEO_Keywords() 109 | { 110 | 111 | if (defined("useSeo")) { 112 | echo useSeo["keywords"]; 113 | return; 114 | } 115 | if (class_exists("useSeo")) { 116 | useSeo::Keywords(); 117 | return; 118 | } 119 | 120 | if (Get::Is("index")) { 121 | Get::Options("keywords", true); 122 | } elseif (Get::Is("post")) { 123 | TTDF_SEO::Category(); ?>,getArchiveSlug(); // 获取当前分类的 slug 163 | $category = $db->fetchRow( 164 | $db 165 | ->select("description") 166 | ->from("table.metas") 167 | ->where("slug = ?", $slug) 168 | ->where("type = ?", "category") 169 | ); 170 | if (!empty($category["description"])) { 171 | $description = str_replace( 172 | ["\r", "\n"], 173 | "", 174 | strip_tags($category["description"]) 175 | ); // 去除换行符和 HTML 标签 176 | $description = preg_replace( 177 | '/(rrel|rel|canonical|nofollow|noindex)="[^"]*"/i', 178 | "", 179 | $description 180 | ); // 移除类似标签 181 | echo $description; 182 | } else { 183 | Get::Options("description", true); 184 | } 185 | } else { 186 | Get::Options("description", true); 187 | } 188 | } 189 | ?> 190 | <?php TTDF_SEO_Title(); ?> 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /assets/_ttdf/motyf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MotyfJS 3 | * 基于Notyf修改的轻量级消息提示组件 4 | * @author 鼠子Tomoriゞ 5 | * @link https://github.com/YuiNijika/MotynJS 6 | */ 7 | class MotyfJS { 8 | constructor(autoCloseDefault = true) { 9 | this.autoCloseDefault = autoCloseDefault; 10 | this.timers = new Map(); // 存储定时器 11 | this.initContainer(); 12 | } 13 | 14 | /** 15 | * 初始化消息容器 16 | */ 17 | initContainer() { 18 | if (!document.querySelector('.motym')) { 19 | const container = document.createElement('div'); 20 | container.className = 'motym'; 21 | document.body.appendChild(container); 22 | } 23 | } 24 | 25 | /** 26 | * 设置消息位置 27 | * @param {string} position - 位置参数 28 | */ 29 | setPosition(position) { 30 | const container = document.querySelector('.motym'); 31 | if (container) { 32 | container.className = 'motym ' + position; 33 | } 34 | } 35 | 36 | /** 37 | * 创建或更新消息元素 38 | * @private 39 | */ 40 | createOrUpdateMessage(content, type, id) { 41 | let messageElement; 42 | 43 | if (id && document.getElementById(id)) { 44 | // 更新现有消息 45 | messageElement = document.getElementById(id); 46 | const motyfElement = messageElement.querySelector('.motyf'); 47 | if (motyfElement) { 48 | motyfElement.className = 'motyf ' + type; 49 | const iconHtml = this.getIconHtml(type); 50 | motyfElement.innerHTML = `${iconHtml}${content}`; 51 | messageElement.classList.remove('motym-out'); 52 | void messageElement.offsetWidth; // 触发重绘 53 | } 54 | } else { 55 | // 创建新消息 56 | messageElement = document.createElement('div'); 57 | messageElement.className = 'motyf-message'; 58 | if (id) messageElement.id = id; 59 | 60 | const iconHtml = this.getIconHtml(type); 61 | messageElement.innerHTML = ` 62 |
63 | ${iconHtml} 64 | ${content} 65 |
66 | `; 67 | document.querySelector('.motym').appendChild(messageElement); 68 | 69 | // 触发入场动画 70 | void messageElement.offsetWidth; 71 | messageElement.classList.add('motyf-enter'); 72 | setTimeout(() => messageElement.classList.remove('motyf-enter'), 400); 73 | } 74 | 75 | return messageElement; 76 | } 77 | 78 | /** 79 | * 显示消息 80 | */ 81 | show(str, type, time, id, autoClose = this.autoCloseDefault) { 82 | let content, messageType, displayTime, messageId, position; 83 | 84 | // 参数解析 85 | if (typeof str === 'object') { 86 | content = str.content || str.str || ''; 87 | messageType = str.type || 'success'; 88 | displayTime = str.time ?? 3000; 89 | messageId = str.id || id; 90 | position = str.position; 91 | autoClose = str.autoClose ?? autoClose; 92 | } else { 93 | content = str || ''; 94 | messageType = type || 'success'; 95 | displayTime = time ?? 3000; 96 | messageId = id; 97 | } 98 | 99 | this.initContainer(); 100 | if (position) this.setPosition(position); 101 | 102 | // 清除旧定时器 103 | if (messageId && this.timers.has(messageId)) { 104 | clearTimeout(this.timers.get(messageId)); 105 | this.timers.delete(messageId); 106 | } 107 | 108 | // 创建/更新消息 109 | const messageElement = this.createOrUpdateMessage(content, messageType, messageId); 110 | 111 | // 设置自动关闭 112 | if (autoClose && displayTime > 0) { 113 | const timerId = setTimeout(() => { 114 | this.close(messageElement); 115 | if (messageId) this.timers.delete(messageId); 116 | }, displayTime); 117 | 118 | if (messageId) this.timers.set(messageId, timerId); 119 | } 120 | 121 | return messageElement; 122 | } 123 | 124 | /** 125 | * 获取图标HTML 126 | * @private 127 | */ 128 | getIconHtml(type) { 129 | const icons = { 130 | success: '', 131 | error: '', 132 | warning: '', 133 | info: '' 134 | }; 135 | return `${icons[type] || ''}`; 136 | } 137 | 138 | /** 139 | * 关闭消息 140 | */ 141 | close(element) { 142 | if (element) { 143 | // 清除关联定时器 144 | if (element.id && this.timers.has(element.id)) { 145 | clearTimeout(this.timers.get(element.id)); 146 | this.timers.delete(element.id); 147 | } 148 | 149 | element.classList.add('motym-out'); 150 | setTimeout(() => element.remove(), 300); 151 | } 152 | } 153 | 154 | /** 155 | * 关闭所有消息 156 | */ 157 | closeAll() { 158 | document.querySelectorAll('.motyf-message').forEach(msg => this.close(msg)); 159 | this.timers.forEach(timer => clearTimeout(timer)); 160 | this.timers.clear(); 161 | } 162 | 163 | // 快捷方法 164 | success(str, time, id, autoClose) { 165 | return this.show(str, 'success', time, id, autoClose); 166 | } 167 | 168 | error(str, time, id, autoClose) { 169 | return this.show(str, 'error', time, id, autoClose); 170 | } 171 | 172 | warning(str, time, id, autoClose) { 173 | return this.show(str, 'warning', time, id, autoClose); 174 | } 175 | 176 | info(str, time, id, autoClose) { 177 | return this.show(str, 'info', time, id, autoClose); 178 | } 179 | } 180 | 181 | // 全局实例 182 | const motyfInstance = new MotyfJS(); 183 | 184 | // 全局函数 185 | window.motyf = function (str, type, time, id, autoClose) { 186 | return motyfInstance.show(str, type, time, id, autoClose); 187 | }; 188 | 189 | // 快捷方法挂载 190 | window.motyf.success = (...args) => motyfInstance.success(...args); 191 | window.motyf.error = (...args) => motyfInstance.error(...args); 192 | window.motyf.warning = (...args) => motyfInstance.warning(...args); 193 | window.motyf.info = (...args) => motyfInstance.info(...args); 194 | window.motyf_close = (el) => motyfInstance.close(el); 195 | window.MotyfJS = MotyfJS; 196 | 197 | // 点击关闭事件 198 | document.addEventListener('click', (e) => { 199 | const msg = e.target.closest('.motyf-message'); 200 | if (msg) motyfInstance.close(msg); 201 | }); -------------------------------------------------------------------------------- /assets/welcome.css: -------------------------------------------------------------------------------- 1 | .ttdf-loader-bar { 2 | background: #00dc82; 3 | bottom: 0; 4 | height: 3px; 5 | left: 0; 6 | position: fixed; 7 | right: 0 8 | } 9 | 10 | /* 基础定位与颜色 */ 11 | .triangle-loading { 12 | position: absolute; 13 | color: rgba(0, 220, 130, 0.8); 14 | /* 初始颜色 #00DC82/80 */ 15 | } 16 | 17 | /* 父容器hover时颜色加深 */ 18 | .ttdf-logo:hover .triangle-loading { 19 | color: #00DC82; 20 | } 21 | 22 | /* 路径动画核心样式 */ 23 | .triangle-loading path { 24 | fill: none; 25 | stroke: currentColor; 26 | stroke-width: 35px; 27 | stroke-linecap: round; 28 | stroke-linejoin: round; 29 | stroke-dasharray: 2800; 30 | stroke-dashoffset: 2800; 31 | animation: svg-stroke-loop 3s linear infinite; 32 | } 33 | 34 | .ttdf-logo:hover .triangle-loading path { 35 | animation-play-state: paused; 36 | } 37 | 38 | @keyframes svg-stroke-loop { 39 | 0% { 40 | stroke-dashoffset: 2800; 41 | } 42 | 43 | /* 初始隐藏 */ 44 | 85% { 45 | stroke-dashoffset: 0; 46 | } 47 | 48 | /* 85%时画完整个图形 */ 49 | 100% { 50 | stroke-dashoffset: -2800; 51 | } 52 | } 53 | 54 | @keyframes ttdf-loading-move { 55 | to { 56 | stroke-dashoffset: -128 57 | } 58 | } 59 | 60 | @media (prefers-color-scheme:dark) { 61 | 62 | body, 63 | html { 64 | color: #fff; 65 | color-scheme: dark 66 | } 67 | } 68 | 69 | *, 70 | :after, 71 | :before { 72 | border-color: var(--un-default-border-color, #e5e7eb); 73 | border-style: solid; 74 | border-width: 0; 75 | box-sizing: border-box 76 | } 77 | 78 | :after, 79 | :before { 80 | --un-content: "" 81 | } 82 | 83 | html { 84 | line-height: 1.5; 85 | -webkit-text-size-adjust: 100%; 86 | font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; 87 | font-feature-settings: normal; 88 | font-variation-settings: normal; 89 | -moz-tab-size: 4; 90 | tab-size: 4; 91 | -webkit-tap-highlight-color: transparent 92 | } 93 | 94 | body { 95 | line-height: inherit; 96 | margin: 0 97 | } 98 | 99 | a { 100 | color: inherit; 101 | text-decoration: inherit 102 | } 103 | 104 | svg { 105 | display: block; 106 | vertical-align: middle 107 | } 108 | 109 | *, 110 | :after, 111 | :before { 112 | --un-rotate: 0; 113 | --un-rotate-x: 0; 114 | --un-rotate-y: 0; 115 | --un-rotate-z: 0; 116 | --un-scale-x: 1; 117 | --un-scale-y: 1; 118 | --un-scale-z: 1; 119 | --un-skew-x: 0; 120 | --un-skew-y: 0; 121 | --un-translate-x: 0; 122 | --un-translate-y: 0; 123 | --un-translate-z: 0; 124 | --un-pan-x: ; 125 | --un-pan-y: ; 126 | --un-pinch-zoom: ; 127 | --un-scroll-snap-strictness: proximity; 128 | --un-ordinal: ; 129 | --un-slashed-zero: ; 130 | --un-numeric-figure: ; 131 | --un-numeric-spacing: ; 132 | --un-numeric-fraction: ; 133 | --un-border-spacing-x: 0; 134 | --un-border-spacing-y: 0; 135 | --un-ring-offset-shadow: 0 0 transparent; 136 | --un-ring-shadow: 0 0 transparent; 137 | --un-shadow-inset: ; 138 | --un-shadow: 0 0 transparent; 139 | --un-ring-inset: ; 140 | --un-ring-offset-width: 0px; 141 | --un-ring-offset-color: #fff; 142 | --un-ring-width: 0px; 143 | --un-ring-color: rgba(147, 197, 253, .5); 144 | --un-blur: ; 145 | --un-brightness: ; 146 | --un-contrast: ; 147 | --un-drop-shadow: ; 148 | --un-grayscale: ; 149 | --un-hue-rotate: ; 150 | --un-invert: ; 151 | --un-saturate: ; 152 | --un-sepia: ; 153 | --un-backdrop-blur: ; 154 | --un-backdrop-brightness: ; 155 | --un-backdrop-contrast: ; 156 | --un-backdrop-grayscale: ; 157 | --un-backdrop-hue-rotate: ; 158 | --un-backdrop-invert: ; 159 | --un-backdrop-opacity: ; 160 | --un-backdrop-saturate: ; 161 | --un-backdrop-sepia: 162 | } 163 | 164 | .relative { 165 | position: relative 166 | } 167 | 168 | .inline-block { 169 | display: inline-block 170 | } 171 | 172 | .min-h-screen { 173 | min-height: 100vh 174 | } 175 | 176 | .flex { 177 | display: flex 178 | } 179 | 180 | .flex-col { 181 | flex-direction: column 182 | } 183 | 184 | .items-end { 185 | align-items: flex-end 186 | } 187 | 188 | .items-center { 189 | align-items: center 190 | } 191 | 192 | .justify-center { 193 | justify-content: center 194 | } 195 | 196 | .gap-4 { 197 | gap: 1rem 198 | } 199 | 200 | .overflow-hidden { 201 | overflow: hidden 202 | } 203 | 204 | .border { 205 | border-width: 1px 206 | } 207 | 208 | .border-\[\#00DC42\]\/50 { 209 | border-color: #00dc4280 210 | } 211 | 212 | .group:hover .group-hover\:border-\[\#00DC42\] { 213 | --un-border-opacity: 1; 214 | border-color: rgb(0 220 66/var(--un-border-opacity)) 215 | } 216 | 217 | .rounded { 218 | border-radius: .25rem 219 | } 220 | 221 | .bg-\[\#00DC42\]\/10 { 222 | background-color: #00dc421a 223 | } 224 | 225 | .bg-white { 226 | --un-bg-opacity: 1; 227 | background-color: rgb(255 255 255/var(--un-bg-opacity)) 228 | } 229 | 230 | .group:hover .group-hover\:bg-\[\#00DC42\]\/15 { 231 | background-color: #00dc4226 232 | } 233 | 234 | .px-2\.5 { 235 | padding-left: .625rem; 236 | padding-right: .625rem 237 | } 238 | 239 | .py-1\.5 { 240 | padding-bottom: .375rem; 241 | padding-top: .375rem 242 | } 243 | 244 | .text-center { 245 | text-align: center 246 | } 247 | 248 | .text-\[16px\] { 249 | font-size: 16px 250 | } 251 | 252 | .group:hover .group-hover\:text-\[\#00DC82\], 253 | .text-\[\#00DC82\] { 254 | --un-text-opacity: 1; 255 | color: rgb(0 220 130/var(--un-text-opacity)) 256 | } 257 | 258 | .text-\[\#00DC82\]\/80 { 259 | color: #00dc82cc 260 | } 261 | 262 | .group:hover .group-hover\:text-\[\#020420\], 263 | .text-\[\#020420\] { 264 | --un-text-opacity: 1; 265 | color: rgb(2 4 32/var(--un-text-opacity)) 266 | } 267 | 268 | .text-\[\#020420\]\/80 { 269 | color: #020420cc 270 | } 271 | 272 | .font-semibold { 273 | font-weight: 600 274 | } 275 | 276 | .leading-none { 277 | line-height: 1 278 | } 279 | 280 | .font-mono { 281 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace 282 | } 283 | 284 | .font-sans { 285 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji 286 | } 287 | 288 | .antialiased { 289 | -webkit-font-smoothing: antialiased; 290 | -moz-osx-font-smoothing: grayscale 291 | } 292 | 293 | @media (prefers-color-scheme:dark) { 294 | .dark\:bg-\[\#020420\] { 295 | --un-bg-opacity: 1; 296 | background-color: rgb(2 4 32/var(--un-bg-opacity)) 297 | } 298 | 299 | .dark\:text-gray-200 { 300 | --un-text-opacity: 1; 301 | color: rgb(224 224 224/var(--un-text-opacity)) 302 | } 303 | 304 | .dark\:text-white, 305 | .group:hover .dark\:group-hover\:text-white { 306 | --un-text-opacity: 1; 307 | color: rgb(255 255 255/var(--un-text-opacity)) 308 | } 309 | } -------------------------------------------------------------------------------- /core/Modules/Options.php: -------------------------------------------------------------------------------- 1 | $value) { 53 | $element->input->setAttribute($attr, $value); 54 | } 55 | } 56 | 57 | $layout->addItem($element); 58 | } 59 | } 60 | } 61 | 62 | // 辅助类用于输出HTML 63 | class EchoHtml extends Typecho_Widget_Helper_Layout 64 | { 65 | public function __construct($html) 66 | { 67 | $this->html($html); 68 | $this->start(); 69 | $this->end(); 70 | } 71 | public function start() {} 72 | public function end() {} 73 | } 74 | 75 | /** 76 | * 获取字段的当前值 77 | */ 78 | function TTDF_GetFieldValue($field) 79 | { 80 | // 检查字段是否有name属性 81 | if (!isset($field['name']) || empty($field['name'])) { 82 | return $field['value'] ?? ''; 83 | } 84 | 85 | $dbValue = TTDF_Db::getTtdf($field['name']); 86 | 87 | if ($dbValue !== null) { 88 | // 对于复选框、Tags、AddList和DialogSelect,需要特殊处理比较 89 | if (in_array($field['type'], ['Checkbox', 'Tags', 'AddList', 'DialogSelect'])) { 90 | $setupDefault = is_array($field['value']) ? implode(',', $field['value']) : $field['value']; 91 | $dbValueForCompare = $dbValue; 92 | 93 | // 标准化比较去除空格并排序 94 | $setupNormalized = $setupDefault; 95 | $dbNormalized = $dbValueForCompare; 96 | 97 | if (!empty($setupNormalized)) { 98 | $setupArray = explode(',', $setupNormalized); 99 | $setupArray = array_map('trim', $setupArray); 100 | sort($setupArray); 101 | $setupNormalized = implode(',', $setupArray); 102 | } 103 | 104 | if (!empty($dbNormalized)) { 105 | $dbArray = explode(',', $dbNormalized); 106 | $dbArray = array_map('trim', $dbArray); 107 | sort($dbArray); 108 | $dbNormalized = implode(',', $dbArray); 109 | } 110 | 111 | if ($dbNormalized !== $setupNormalized) { 112 | return $dbValue; 113 | } 114 | } 115 | // 对于Switch类型,需要特殊处理布尔值比较 116 | else if ($field['type'] === 'Switch') { 117 | $setupDefault = $field['value'] ?? false; 118 | // 将数据库中的字符串值转换为布尔值进行比较 119 | $dbBoolValue = ($dbValue === 'true' || $dbValue === '1' || $dbValue === true); 120 | $setupBoolValue = ($setupDefault === true || $setupDefault === 'true' || $setupDefault === '1'); 121 | 122 | if ($dbBoolValue !== $setupBoolValue) { 123 | return $dbValue; 124 | } 125 | } 126 | // 对于Number和Slider类型,需要特殊处理数字比较 127 | else if (in_array($field['type'], ['Number', 'Slider'])) { 128 | $setupDefault = $field['value'] ?? 0; 129 | // 将数据库中的字符串值转换为数字进行比较 130 | $dbNumValue = is_numeric($dbValue) ? (float)$dbValue : 0; 131 | $setupNumValue = is_numeric($setupDefault) ? (float)$setupDefault : 0; 132 | 133 | if ($dbNumValue !== $setupNumValue) { 134 | return $dbValue; 135 | } 136 | } else { 137 | if ($dbValue !== $field['value']) { 138 | return $dbValue; 139 | } 140 | } 141 | } 142 | 143 | return $field['value'] ?? ''; 144 | } 145 | 146 | function themeConfig($form) 147 | { 148 | // 处理AJAX保存请求 149 | if (isset($_POST['action']) && $_POST['action'] === 'save_settings') { 150 | $response = array('success' => false, 'message' => ''); 151 | 152 | try { 153 | // 获取所有POST数据 154 | $settings = $_POST; 155 | unset($settings['action']); // 移除action字段 156 | 157 | // 保存设置到数据库 158 | foreach ($settings as $key => $value) { 159 | if (is_array($value)) { 160 | $value = implode(',', $value); 161 | } 162 | // 保存到数据库 163 | TTDF_Db::setTtdf($key, $value); 164 | } 165 | 166 | $response['success'] = true; 167 | $response['message'] = '设置保存成功!'; 168 | } catch (Exception $e) { 169 | $response['message'] = '保存失败:' . $e->getMessage(); 170 | } 171 | 172 | header('Content-Type: application/json'); 173 | echo json_encode($response); 174 | exit; 175 | } 176 | 177 | // 处理表单提交 178 | if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ttdf_ajax_save'])) { 179 | // 禁用所有可能的重定向和额外输出 180 | ob_clean(); 181 | header('Content-Type: application/json'); 182 | 183 | try { 184 | $setupFile = __DIR__ . '/../../app/setup.php'; 185 | if (!file_exists($setupFile)) { 186 | $setupFile = __DIR__ . '/../../app/Setup.php'; 187 | } 188 | // 获取所有设置项 189 | $tabs = require $setupFile; 190 | 191 | // 遍历所有设置项并保存 192 | foreach ($tabs as $tab) { 193 | if (isset($tab['fields'])) { 194 | foreach ($tab['fields'] as $field) { 195 | if (isset($field['name']) && $field['type'] !== 'Html') { 196 | // 直接从$_POST中获取原始字段名的值 197 | $value = $_POST[$field['name']] ?? null; 198 | 199 | // 处理复选框的多值情况 200 | if (is_array($value)) { 201 | $value = implode(',', $value); 202 | } 203 | 204 | // 保存到数据库 205 | TTDF_Db::setTtdf($field['name'], $value); 206 | } 207 | } 208 | } 209 | } 210 | 211 | echo json_encode(['success' => true, 'message' => '设置已保存!']); 212 | } catch (Exception $e) { 213 | echo json_encode(['success' => false, 'message' => '保存失败: ' . $e->getMessage()]); 214 | } 215 | 216 | exit; 217 | } 218 | 219 | $versionParam = ''; 220 | if (defined('__FRAMEWORK_VER__')) { 221 | $versionParam = '?Ver=' . __FRAMEWORK_VER__; 222 | } 223 | 224 | // CSS文件数组 225 | $cssFiles = [ 226 | 'core/Static/Element.css', 227 | 'core/Static/Options.css' 228 | ]; 229 | 230 | // 输出CSS文件 231 | foreach ($cssFiles as $cssFile) { 232 | echo '' . "\n"; 235 | } 236 | ?> 237 | 238 |
239 | 240 | get_site_url(false) . __TTDF_RESTAPI_ROUTE__ . '/ttdf', 243 | ]; 244 | ?> 245 | 246 | 249 | 250 | ' . "\n"; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /core/Modules/Rest/Router.php: -------------------------------------------------------------------------------- 1 | request = $request; 25 | $this->response = $response; 26 | $this->db = $db; 27 | $this->formatter = $formatter; 28 | } 29 | 30 | private function handleNotFound(string $endpoint): never 31 | { 32 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 33 | TTDF_Debug::logApiProcess('ENDPOINT_NOT_FOUND', ['endpoint' => $endpoint]); 34 | } 35 | $this->response->error('Endpoint not found', HttpCode::NOT_FOUND); 36 | } 37 | 38 | public function handleRequest(): never 39 | { 40 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 41 | TTDF_Debug::logApiProcess('HANDLE_REQUEST_START'); 42 | } 43 | 44 | try { 45 | // 处理OPTIONS预检请求 46 | if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') { 47 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 48 | TTDF_Debug::logApiProcess('HANDLING_OPTIONS_REQUEST'); 49 | } 50 | $this->response->send([], HttpCode::OK); 51 | } 52 | 53 | // 允许 GET 和 POST 方法 54 | if (!in_array($_SERVER['REQUEST_METHOD'] ?? 'GET', ['GET', 'POST'])) { 55 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 56 | TTDF_Debug::logApiProcess('METHOD_NOT_ALLOWED', [ 57 | 'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN' 58 | ]); 59 | } 60 | $this->response->error('Method Not Allowed', HttpCode::METHOD_NOT_ALLOWED); 61 | } 62 | 63 | $endpoint = $this->request->pathParts[0] ?? ''; 64 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 65 | TTDF_Debug::logApiProcess('DETERMINING_ENDPOINT', ['endpoint' => $endpoint]); 66 | } 67 | 68 | // 检查限制逻辑 69 | $this->checkRestrictions($endpoint); 70 | 71 | $data = match ($endpoint) { 72 | '' => $this->handleIndex(), 73 | 'index' => $this->handleIndex(), 74 | 'posts' => $this->handlePostList(), 75 | 'pages' => $this->handlePageList(), 76 | 'content' => $this->handlePostContent(), 77 | 'category' => $this->handleCategory(), 78 | 'tag' => $this->handleTag(), 79 | 'search' => $this->handleSearch(), 80 | 'options' => $this->handleOptions(), 81 | 'fields' => $this->handleFieldSearch(), 82 | 'advancedFields' => $this->handleAdvancedFieldSearch(), 83 | 'comments' => ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' 84 | ? $this->handlePostComment() 85 | : $this->handleComments(), 86 | 'attachments' => $this->handleAttachmentList(), 87 | 'ttdf' => $this->handleTtdf(), 88 | default => $this->handleNotFound($endpoint), 89 | }; 90 | 91 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 92 | TTDF_Debug::logApiProcess('SENDING_RESPONSE_DATA', [ 93 | 'endpoint' => $endpoint, 94 | 'data_size' => strlen(json_encode($data, JSON_UNESCAPED_UNICODE)) 95 | ]); 96 | } 97 | $this->response->send($data); 98 | } catch (Throwable $e) { 99 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 100 | TTDF_Debug::logApiError('Error in handleRequest', $e); 101 | } 102 | $this->response->error('Internal Server Error', HttpCode::INTERNAL_ERROR, $e); 103 | } 104 | } 105 | 106 | private function checkRestrictions(string $endpoint): void 107 | { 108 | $limitConfig = TTDF_CONFIG['REST_API']['LIMIT'] ?? []; 109 | 110 | $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; 111 | 112 | // 检查GET请求限制 113 | if ($method === 'GET' && !empty($limitConfig['GET'])) { 114 | $restrictedEndpoints = explode(',', $limitConfig['GET']); 115 | if (in_array($endpoint, $restrictedEndpoints)) { 116 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 117 | TTDF_Debug::logApiProcess('ACCESS_FORBIDDEN', [ 118 | 'endpoint' => $endpoint, 119 | 'method' => $method, 120 | 'reason' => 'GET request forbidden' 121 | ]); 122 | } 123 | $this->response->error('Access Forbidden', HttpCode::FORBIDDEN); 124 | } 125 | } 126 | 127 | // 检查POST请求限制 128 | if ($method === 'POST' && !empty($limitConfig['POST'])) { 129 | $restrictedEndpoints = explode(',', $limitConfig['POST']); 130 | if (in_array($endpoint, $restrictedEndpoints)) { 131 | if ((TTDF_CONFIG['DEBUG'] ?? false) && class_exists('TTDF_Debug')) { 132 | TTDF_Debug::logApiProcess('ACCESS_FORBIDDEN', [ 133 | 'endpoint' => $endpoint, 134 | 'method' => $method, 135 | 'reason' => 'POST request forbidden' 136 | ]); 137 | } 138 | $this->response->error('Access Forbidden', HttpCode::FORBIDDEN); 139 | } 140 | } 141 | } 142 | 143 | private function handleIndex(): array 144 | { 145 | $controller = new IndexController($this->request, $this->response, $this->db, $this->formatter); 146 | return $controller->handle(); 147 | } 148 | 149 | private function handlePostList(): array 150 | { 151 | $controller = new PostController($this->request, $this->response, $this->db, $this->formatter); 152 | return $controller->handleList(); 153 | } 154 | 155 | private function handlePageList(): array 156 | { 157 | $controller = new PageController($this->request, $this->response, $this->db, $this->formatter); 158 | return $controller->handleList(); 159 | } 160 | 161 | private function handlePostContent(): array 162 | { 163 | $controller = new PostController($this->request, $this->response, $this->db, $this->formatter); 164 | return $controller->handleContent(); 165 | } 166 | 167 | private function handleCategory(): array 168 | { 169 | $controller = new CategoryController($this->request, $this->response, $this->db, $this->formatter); 170 | return $controller->handle(); 171 | } 172 | 173 | private function handleTag(): array 174 | { 175 | $controller = new TagController($this->request, $this->response, $this->db, $this->formatter); 176 | return $controller->handle(); 177 | } 178 | 179 | private function handleSearch(): array 180 | { 181 | $controller = new SearchController($this->request, $this->response, $this->db, $this->formatter); 182 | return $controller->handle(); 183 | } 184 | 185 | private function handleOptions(): array 186 | { 187 | $controller = new OptionController($this->request, $this->response, $this->db, $this->formatter); 188 | return $controller->handleOptions(); 189 | } 190 | 191 | private function handleFieldSearch(): array 192 | { 193 | $controller = new FieldController($this->request, $this->response, $this->db, $this->formatter); 194 | return $controller->handleFieldSearch(); 195 | } 196 | 197 | private function handleAdvancedFieldSearch(): array 198 | { 199 | $controller = new FieldController($this->request, $this->response, $this->db, $this->formatter); 200 | return $controller->handleAdvancedFieldSearch(); 201 | } 202 | 203 | private function handleComments(): array 204 | { 205 | $controller = new CommentController($this->request, $this->response, $this->db, $this->formatter); 206 | return $controller->handle(); 207 | } 208 | 209 | private function handlePostComment(): array 210 | { 211 | $controller = new CommentController($this->request, $this->response, $this->db, $this->formatter); 212 | return $controller->handlePostComment(); 213 | } 214 | 215 | private function handleAttachmentList(): array 216 | { 217 | $controller = new AttachmentController($this->request, $this->response, $this->db, $this->formatter); 218 | return $controller->handleList(); 219 | } 220 | 221 | private function handleTtdf(): array 222 | { 223 | $controller = new TTDFController($this->request, $this->response, $this->db, $this->formatter); 224 | return $controller->handle(); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /core/Widget/OOP/User.php: -------------------------------------------------------------------------------- 1 | init(); 35 | } 36 | } 37 | 38 | /** 39 | * 获取用户ID 40 | * @param bool $echo 是否直接输出 41 | * @return int|void 42 | */ 43 | public static function Uid(bool $echo = true) 44 | { 45 | try { 46 | self::initErrorHandler(); 47 | 48 | // 检查缓存 49 | $cacheKey = 'uid'; 50 | if (isset(self::$cache[$cacheKey])) { 51 | $uid = self::$cache[$cacheKey]; 52 | } else { 53 | $uid = self::getArchive()->author->uid ?? 0; 54 | self::$cache[$cacheKey] = $uid; 55 | } 56 | 57 | if ($echo) { 58 | echo $uid; 59 | } else { 60 | return $uid; 61 | } 62 | } catch (Exception $e) { 63 | self::$errorHandler->error('获取作者UID失败', [], $e); 64 | if ($echo) { 65 | echo '0'; 66 | } else { 67 | return 0; 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * 判断登录状态 74 | * @param bool $echo 是否直接输出 75 | * @return bool|void 76 | */ 77 | public static function Login(bool $echo = true) 78 | { 79 | try { 80 | self::initErrorHandler(); 81 | 82 | // 检查缓存 83 | $cacheKey = 'login_status'; 84 | if (isset(self::$cache[$cacheKey])) { 85 | $isLoggedIn = self::$cache[$cacheKey]; 86 | } else { 87 | $user = Typecho_Widget::widget('Widget_User'); 88 | $isLoggedIn = $user->hasLogin(); 89 | self::$cache[$cacheKey] = $isLoggedIn; 90 | } 91 | 92 | if ($echo) { 93 | echo $isLoggedIn ? 'true' : 'false'; 94 | } else { 95 | return $isLoggedIn; 96 | } 97 | } catch (Exception $e) { 98 | self::$errorHandler->error('获取用户登录状态失败', [], $e); 99 | if ($echo) { 100 | echo 'false'; 101 | } else { 102 | return false; 103 | } 104 | } 105 | } 106 | 107 | // 获取用户名 108 | public static function Name($echo = true) 109 | { 110 | try { 111 | $author = self::getArchive()->author->screenName; 112 | if ($echo) { 113 | echo $author; 114 | } else { 115 | return $author; 116 | } 117 | } catch (Exception $e) { 118 | self::handleError('获取作者失败', $e); 119 | if ($echo) { 120 | echo ''; 121 | } else { 122 | return ''; 123 | } 124 | } 125 | } 126 | 127 | // 获取昵称 128 | public static function DisplayName($echo = true) 129 | { 130 | try { 131 | $name = self::getArchive()->author->name; 132 | if ($echo) { 133 | echo $name; 134 | } else { 135 | return $name; 136 | } 137 | } catch (Exception $e) { 138 | self::handleError('获取昵称失败', $e); 139 | if ($echo) { 140 | echo ''; 141 | } else { 142 | return ''; 143 | } 144 | } 145 | } 146 | 147 | // 获取用户头像 148 | public static function Avatar($size = 128, $echo = true) 149 | { 150 | try { 151 | $avatar = self::getArchive()->author->gravatar($size); 152 | if ($echo) { 153 | echo $avatar; 154 | } else { 155 | return $avatar; 156 | } 157 | } catch (Exception $e) { 158 | self::handleError('获取作者头像失败', $e); 159 | if ($echo) { 160 | echo ''; 161 | } else { 162 | return ''; 163 | } 164 | } 165 | } 166 | 167 | // 获取用户头像URL 168 | public static function AvatarURL($size = 128, $default = 'mm', $rating = 'X', $echo = true) 169 | { 170 | try { 171 | $email = self::getArchive()->author->mail; 172 | $isSecure = self::getArchive()->request->isSecure(); 173 | $avatarUrl = \Typecho\Common::gravatarUrl($email, $size, $rating, $default, $isSecure); 174 | 175 | if ($echo) { 176 | echo $avatarUrl; 177 | } else { 178 | return $avatarUrl; 179 | } 180 | } catch (Exception $e) { 181 | self::handleError('获取作者头像URL失败', $e); 182 | if ($echo) { 183 | echo ''; 184 | } else { 185 | return ''; 186 | } 187 | } 188 | } 189 | 190 | // 获取用户邮箱 191 | public static function Email($echo = true) 192 | { 193 | try { 194 | $email = self::getArchive()->author->mail; 195 | if ($echo) { 196 | echo $email; 197 | } else { 198 | return $email; 199 | } 200 | } catch (Exception $e) { 201 | self::handleError('获取作者邮箱失败', $e); 202 | if ($echo) { 203 | echo ''; 204 | } else { 205 | return ''; 206 | } 207 | } 208 | } 209 | 210 | // 获取用户网站 211 | public static function WebSite($echo = true) 212 | { 213 | try { 214 | $url = self::getArchive()->author->url; 215 | if ($echo) { 216 | echo $url; 217 | } else { 218 | return $url; 219 | } 220 | } catch (Exception $e) { 221 | self::handleError('获取作者网站失败', $e); 222 | if ($echo) { 223 | echo ''; 224 | } else { 225 | return ''; 226 | } 227 | } 228 | } 229 | 230 | // 获取用户组/角色 231 | public static function Role($echo = true) 232 | { 233 | try { 234 | $group = self::getArchive()->author->group; 235 | if ($echo) { 236 | echo $group; 237 | } else { 238 | return $group; 239 | } 240 | } catch (Exception $e) { 241 | self::handleError('获取作者组失败', $e); 242 | if ($echo) { 243 | echo ''; 244 | } else { 245 | return ''; 246 | } 247 | } 248 | } 249 | 250 | // 获取注册时间 251 | public static function Registered($format = 'Y-m-d H:i:s', $echo = true) 252 | { 253 | try { 254 | $time = self::getArchive()->author->created; 255 | $formatted = date($format, $time); 256 | if ($echo) { 257 | echo $formatted; 258 | } else { 259 | return $formatted; 260 | } 261 | } catch (Exception $e) { 262 | self::handleError('获取注册时间失败', $e); 263 | if ($echo) { 264 | echo ''; 265 | } else { 266 | return ''; 267 | } 268 | } 269 | } 270 | 271 | // 获取最后登录时间 272 | public static function LastLogin($format = 'Y-m-d H:i:s', $echo = true) 273 | { 274 | try { 275 | $time = self::getArchive()->author->logged; 276 | $formatted = date($format, $time); 277 | if ($echo) { 278 | echo $formatted; 279 | } else { 280 | return $formatted; 281 | } 282 | } catch (Exception $e) { 283 | self::handleError('获取最后登录时间失败', $e); 284 | if ($echo) { 285 | echo ''; 286 | } else { 287 | return ''; 288 | } 289 | } 290 | } 291 | 292 | // 获取文章数 293 | public static function PostCount($echo = true) 294 | { 295 | try { 296 | $count = self::getArchive()->author->postsNum; 297 | if ($echo) { 298 | echo $count; 299 | } else { 300 | return $count; 301 | } 302 | } catch (Exception $e) { 303 | self::handleError('获取文章数失败', $e); 304 | if ($echo) { 305 | echo '0'; 306 | } else { 307 | return 0; 308 | } 309 | } 310 | } 311 | 312 | // 获取页面数量 313 | public static function PageCount($echo = true) 314 | { 315 | try { 316 | $db = \Typecho\Db::get(); 317 | $count = $db->fetchObject($db->select(['COUNT(cid)' => 'num']) 318 | ->from('table.contents') 319 | ->where('type = ?', 'page') 320 | ->where('authorId = ?', self::getArchive()->author->uid) 321 | ->where('status = ?', 'publish'))->num; 322 | 323 | if ($echo) { 324 | echo $count; 325 | } else { 326 | return $count; 327 | } 328 | } catch (Exception $e) { 329 | self::handleError('获取页面数量失败', $e); 330 | if ($echo) { 331 | echo '0'; 332 | } else { 333 | return 0; 334 | } 335 | } 336 | } 337 | 338 | // 获取作者链接 339 | public static function Permalink($echo = true) 340 | { 341 | try { 342 | $permalink = self::getArchive()->author->permalink; 343 | if ($echo) { 344 | echo $permalink; 345 | } else { 346 | return $permalink; 347 | } 348 | } catch (Exception $e) { 349 | self::handleError('获取作者链接失败', $e); 350 | if ($echo) { 351 | echo ''; 352 | } else { 353 | return ''; 354 | } 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /core/Widget/TyAjax.php: -------------------------------------------------------------------------------- 1 | [回调数组]] 27 | */ 28 | public $callbacks = array(); 29 | 30 | /** 31 | * 添加过滤器回调 32 | * 33 | * @param string $tag 钩子名称 34 | * @param callable $function_to_add 回调函数 35 | * @param int $priority 优先级(数字越小越先执行) 36 | * @param int $accepted_args 接收的参数数量 37 | * @return bool 总是返回 true 38 | */ 39 | public function add_filter($tag, $function_to_add, $priority, $accepted_args) 40 | { 41 | $this->callbacks[$priority][] = array( 42 | 'function' => $function_to_add, // 回调函数 43 | 'accepted_args' => $accepted_args // 接收参数数量 44 | ); 45 | return true; 46 | } 47 | 48 | /** 49 | * 执行过滤器链 50 | * 51 | * @param mixed $value 初始值 52 | * @param array $args 参数数组 53 | * @return mixed 经过所有回调处理后的最终值 54 | */ 55 | public function apply_filters($value, $args) 56 | { 57 | // 按优先级排序(从小到大) 58 | ksort($this->callbacks); 59 | 60 | // 遍历所有优先级 61 | foreach ($this->callbacks as $priority => $callbacks) { 62 | // 遍历当前优先级的所有回调 63 | foreach ($callbacks as $callback) { 64 | // 裁剪参数数量 65 | $args = array_slice($args, 0, $callback['accepted_args']); 66 | // 执行回调并更新值 67 | $value = call_user_func_array($callback['function'], $args); 68 | } 69 | } 70 | 71 | return $value; 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * AJAX 核心处理类 78 | * 79 | * 提供静态方法处理 AJAX 请求和资源管理 80 | */ 81 | class TyAjax_Core 82 | { 83 | /** 84 | * 存储所有过滤器 85 | * @var array [钩子名 => TyAjax_Hook 实例] 86 | */ 87 | public static $filters = array(); 88 | 89 | /** 90 | * 存储已注册的动作(Actions) 91 | * @var array [钩子名 => true] 92 | */ 93 | public static $actions = array(); 94 | 95 | /** @var TTDF_ErrorHandler 错误处理器实例 */ 96 | private static $errorHandler; 97 | 98 | 99 | 100 | /** 101 | * 初始化错误处理器和缓存管理器 102 | */ 103 | private static function initErrorHandler(): void 104 | { 105 | if (!self::$errorHandler) { 106 | self::$errorHandler = TTDF_ErrorHandler::getInstance(); 107 | self::$errorHandler->init(); 108 | } 109 | 110 | // 初始化缓存管理器 111 | if (class_exists('TTDF_CacheManager')) { 112 | TTDF_CacheManager::init(); 113 | } 114 | } 115 | 116 | /** 117 | * 初始化 AJAX 系统 118 | * 119 | * - 检测 AJAX 请求并处理 120 | * - 注册资源加载钩子 121 | */ 122 | public static function init() 123 | { 124 | // 检测 AJAX 请求(XMLHttpRequest) 125 | if ( 126 | !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && 127 | strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest' 128 | ) { 129 | self::handle_request(); 130 | exit; 131 | } 132 | } 133 | 134 | /** 135 | * 处理 AJAX 请求 136 | * 137 | * - 验证请求 138 | * - 路由到对应处理函数 139 | * - 格式化响应 140 | */ 141 | private static function handle_request() 142 | { 143 | try { 144 | self::initErrorHandler(); 145 | 146 | // 设置响应头 147 | header('Content-Type: application/json; charset=utf-8'); 148 | header('Cache-Control: no-cache, must-revalidate'); 149 | 150 | // 获取请求数据(支持GET和POST) 151 | $data = array_merge($_GET, $_POST); 152 | 153 | // 验证 action 参数 154 | if (empty($data['action'])) { 155 | self::send_error('缺少action参数', 'danger', 400); 156 | } 157 | 158 | // 过滤和验证action参数 159 | $action = preg_replace('/[^a-zA-Z0-9_]/', '', $data['action']); 160 | if (empty($action)) { 161 | self::send_error('无效的action参数', 'danger', 400); 162 | } 163 | 164 | // 确定钩子名称(区分登录/未登录状态) 165 | $user = Typecho_Widget::widget('Widget_User'); 166 | $is_logged_in = method_exists($user, 'hasLogin') ? $user->hasLogin() : false; 167 | $hook = $is_logged_in ? "ty_ajax_{$action}" : "ty_ajax_nopriv_{$action}"; 168 | 169 | // 检查是否有对应的处理函数 170 | if (!self::has_action($hook)) { 171 | self::$errorHandler->warning('未找到AJAX处理方法', ['action' => $action, 'hook' => $hook]); 172 | self::send_error("未找到{$action}的处理方法", 'danger', 404); 173 | } 174 | 175 | // 执行过滤器链获取响应 176 | $response = self::apply_filters($hook, null, $data); 177 | 178 | // 标准化响应格式 179 | if (!isset($response['error'])) { 180 | $response = [ 181 | 'error' => 0, // 错误码(0=成功) 182 | 'msg' => $response['msg'] ?? '操作成功', // 消息 183 | 'ys' => $response['ys'] ?? '', // 消息样式 184 | 'data' => $response['data'] ?? null // 数据 185 | ]; 186 | } 187 | 188 | // 输出 JSON 响应 189 | echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); 190 | exit; 191 | } catch (Exception $e) { 192 | self::$errorHandler->error('AJAX请求处理失败', ['action' => $data['action'] ?? 'unknown'], $e); 193 | // 捕获异常并返回错误 194 | self::send_error($e->getMessage(), 'danger', $e->getCode() ?: 500); 195 | } 196 | } 197 | 198 | /** 199 | * 添加过滤器/动作 200 | * 201 | * @param string $hook 钩子名称 202 | * @param callable $callback 回调函数 203 | * @param int $priority 优先级 204 | * @param int $accepted_args 接收参数数量 205 | * @return bool 总是返回 true 206 | */ 207 | public static function add_filter($hook, $callback, $priority = 10, $accepted_args = 1) 208 | { 209 | // 初始化钩子对象(如果不存在) 210 | if (!isset(self::$filters[$hook])) { 211 | self::$filters[$hook] = new TyAjax_Hook(); 212 | } 213 | 214 | // 添加回调 215 | self::$filters[$hook]->add_filter($hook, $callback, $priority, $accepted_args); 216 | 217 | // 如果是动作(Action)则记录下来 218 | if (strpos($hook, 'ty_ajax_') === 0) { 219 | self::$actions[$hook] = true; 220 | } 221 | 222 | return true; 223 | } 224 | 225 | /** 226 | * 执行过滤器链 227 | * 228 | * @param string $hook 钩子名称 229 | * @param mixed $value 初始值 230 | * @param mixed ...$args 可变参数 231 | * @return mixed 处理后的值 232 | */ 233 | public static function apply_filters($hook, $value = null, ...$args) 234 | { 235 | // 如果钩子不存在则直接返回值 236 | if (!isset(self::$filters[$hook])) { 237 | return $value; 238 | } 239 | return self::$filters[$hook]->apply_filters($value, $args); 240 | } 241 | 242 | /** 243 | * 检查动作是否存在 244 | * 245 | * @param string $hook 钩子名称 246 | * @return bool 是否存在 247 | */ 248 | public static function has_action($hook) 249 | { 250 | return isset(self::$actions[$hook]); 251 | } 252 | 253 | /** 254 | * 发送成功响应 255 | * 256 | * @param string $msg 消息内容 257 | * @param mixed $data 返回数据 258 | * @param string $ys 消息样式 259 | */ 260 | public static function send_success(string $msg = '操作成功', $data = null, string $ys = ''): void 261 | { 262 | self::initErrorHandler(); 263 | 264 | try { 265 | $response = [ 266 | 'error' => 0, 267 | 'msg' => $msg, 268 | 'ys' => $ys, 269 | 'data' => $data, 270 | 'timestamp' => time() 271 | ]; 272 | 273 | echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); 274 | } catch (Exception $e) { 275 | self::$errorHandler->error('发送成功响应失败', ['msg' => $msg, 'data' => $data], $e); 276 | echo json_encode(['error' => 1, 'msg' => '响应发送失败'], JSON_UNESCAPED_UNICODE); 277 | } 278 | 279 | exit; 280 | } 281 | 282 | /** 283 | * 发送错误响应 284 | * 285 | * @param string $msg 错误消息 286 | * @param string $ys 消息样式 287 | * @param int $status HTTP 状态码 288 | */ 289 | public static function send_error(string $msg = '操作失败', string $ys = 'danger', int $status = 400): void 290 | { 291 | self::initErrorHandler(); 292 | 293 | try { 294 | http_response_code($status); 295 | 296 | $response = [ 297 | 'error' => 1, 298 | 'msg' => $msg, 299 | 'ys' => $ys, 300 | 'timestamp' => time(), 301 | 'status' => $status 302 | ]; 303 | 304 | echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR); 305 | 306 | // 记录错误日志 307 | self::$errorHandler->warning('AJAX错误响应', [ 308 | 'msg' => $msg, 309 | 'status' => $status, 310 | 'ys' => $ys 311 | ]); 312 | } catch (Exception $e) { 313 | self::$errorHandler->error('发送错误响应失败', ['msg' => $msg, 'status' => $status], $e); 314 | http_response_code(500); 315 | echo json_encode(['error' => 1, 'msg' => '服务器内部错误'], JSON_UNESCAPED_UNICODE); 316 | } 317 | 318 | exit; 319 | } 320 | } 321 | 322 | /** 323 | * 快捷函数:添加过滤器 324 | * 325 | * @see TyAjax_Core::add_filter() 326 | */ 327 | if (!function_exists('TyAjax_filter')) { 328 | function TyAjax_filter($hook, $callback, $priority = 10, $accepted_args = 1) 329 | { 330 | return TyAjax_Core::add_filter($hook, $callback, $priority, $accepted_args); 331 | } 332 | } 333 | 334 | /** 335 | * 快捷函数:添加动作 336 | * 337 | * @see TyAjax_Core::add_filter() 338 | */ 339 | if (!function_exists('TyAjax_action')) { 340 | function TyAjax_action($hook, $callback, $priority = 10, $accepted_args = 1) 341 | { 342 | return TyAjax_Core::add_filter($hook, $callback, $priority, $accepted_args); 343 | } 344 | } 345 | 346 | /** 347 | * 快捷函数:执行过滤器 348 | * 349 | * @see TyAjax_Core::apply_filters() 350 | */ 351 | if (!function_exists('TyAjax_apply_filters')) { 352 | function TyAjax_apply_filters($hook, $value = null, ...$args) 353 | { 354 | return TyAjax_Core::apply_filters($hook, $value, ...$args); 355 | } 356 | } 357 | 358 | /** 359 | * 快捷函数:检查动作是否存在 360 | * 361 | * @see TyAjax_Core::has_action() 362 | */ 363 | if (!function_exists('TyAjax_has_action')) { 364 | function TyAjax_has_action($hook) 365 | { 366 | return TyAjax_Core::has_action($hook); 367 | } 368 | } 369 | 370 | /** 371 | * 快捷函数:发送成功响应 372 | * 373 | * @see TyAjax_Core::send_success() 374 | */ 375 | if (!function_exists('TyAjax_send_success')) { 376 | function TyAjax_send_success($msg = '操作成功', $data = null, $ys = '') 377 | { 378 | TyAjax_Core::send_success($msg, $data, $ys); 379 | } 380 | } 381 | 382 | /** 383 | * 快捷函数:发送错误响应 384 | * 385 | * @see TyAjax_Core::send_error() 386 | */ 387 | if (!function_exists('TyAjax_send_error')) { 388 | function TyAjax_send_error($msg = '操作失败', $ys = 'danger', $status = 400) 389 | { 390 | TyAjax_Core::send_error($msg, $ys, $status); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /core/Modules/Rest/Core.php: -------------------------------------------------------------------------------- 1 | path = str_starts_with($requestUri, $basePath) 35 | ? (substr($requestUri, strlen($basePath)) ?: '/') 36 | : '/'; 37 | 38 | $this->pathParts = array_values(array_filter(explode('/', trim($this->path, '/')))); 39 | 40 | $this->contentFormat = ContentFormat::tryFrom(strtolower($this->getQuery('format', 'html'))) ?? ContentFormat::HTML; 41 | 42 | $this->pageSize = max(1, min((int)$this->getQuery('pageSize', 10), 100)); 43 | $this->currentPage = max(1, (int)$this->getQuery('page', 1)); 44 | $this->excerptLength = max(0, (int)$this->getQuery('excerptLength', 200)); 45 | 46 | if (defined('__DEBUG__') && __DEBUG__) { 47 | TTDF_DebugLogger::logApiProcess('APIREQUEST_CONSTRUCT_END', [ 48 | 'path' => $this->path, 49 | 'pageSize' => $this->pageSize, 50 | 'currentPage' => $this->currentPage, 51 | 'excerptLength' => $this->excerptLength 52 | ]); 53 | } 54 | } 55 | 56 | public function getQuery(string $key, mixed $default = null): mixed 57 | { 58 | return $_GET[$key] ?? $default; 59 | } 60 | } 61 | 62 | /** 63 | * 专门负责发送 JSON 响应 64 | */ 65 | final class ApiResponse 66 | { 67 | public function __construct(private ContentFormat $contentFormat) 68 | { 69 | if (defined('__DEBUG__') && __DEBUG__) { 70 | TTDF_DebugLogger::logApiProcess('APIRESPONSE_CONSTRUCT', ['format' => $contentFormat->value]); 71 | } 72 | } 73 | 74 | public function send(array $data = [], HttpCode $code = HttpCode::OK): never 75 | { 76 | if (defined('__DEBUG__') && __DEBUG__) { 77 | TTDF_DebugLogger::logApiProcess('APIRESPONSE_SEND_START', [ 78 | 'code' => $code->value, 79 | 'has_data' => !empty($data) 80 | ]); 81 | } 82 | 83 | try { 84 | if (!headers_sent()) { 85 | \Typecho\Response::getInstance()->setStatus($code->value); 86 | header('Content-Type: application/json; charset=UTF-8'); 87 | $this->setSecurityHeaders(); 88 | } 89 | 90 | $response = [ 91 | 'code' => $code->value, 92 | 'message' => $code === HttpCode::OK ? 'success' : ($data['message'] ?? 'Error'), 93 | 'data' => $data['data'] ?? null, 94 | 'meta' => [ 95 | 'format' => $this->contentFormat->value, 96 | 'timestamp' => time(), 97 | ...($data['meta'] ?? []) 98 | ] 99 | ]; 100 | 101 | $options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR; 102 | if (defined('__DEBUG__') && __DEBUG__) { 103 | $options |= JSON_PRETTY_PRINT; 104 | } 105 | 106 | if (defined('__DEBUG__') && __DEBUG__) { 107 | TTDF_DebugLogger::logApiProcess('SENDING_RESPONSE', [ 108 | 'response_size' => strlen(json_encode($response, $options)) 109 | ]); 110 | } 111 | 112 | echo json_encode($response, $options); 113 | 114 | if (defined('__DEBUG__') && __DEBUG__) { 115 | TTDF_DebugLogger::logApiProcess('RESPONSE_SENT'); 116 | } 117 | exit; 118 | } catch (Throwable $e) { 119 | if (defined('__DEBUG__') && __DEBUG__) { 120 | TTDF_DebugLogger::logApiError('Error in ApiResponse::send', $e); 121 | } 122 | throw $e; 123 | } 124 | } 125 | 126 | public function error(string $message, HttpCode $code, ?Throwable $e = null): never 127 | { 128 | if (defined('__DEBUG__') && __DEBUG__) { 129 | TTDF_DebugLogger::logApiProcess('APIRESPONSE_ERROR', [ 130 | 'message' => $message, 131 | 'code' => $code->value, 132 | 'has_exception' => $e !== null 133 | ]); 134 | } 135 | 136 | $response = ['message' => $message]; 137 | if ($e !== null && (defined('__DEBUG__') && __DEBUG__)) { 138 | $response['error_details'] = [ 139 | 'message' => $e->getMessage(), 140 | 'trace' => $e->getTraceAsString(), 141 | ]; 142 | } 143 | $this->send($response, $code); 144 | } 145 | 146 | private function setSecurityHeaders(): void 147 | { 148 | if (defined('__DEBUG__') && __DEBUG__) { 149 | TTDF_DebugLogger::logApiProcess('SETTING_SECURITY_HEADERS'); 150 | } 151 | 152 | try { 153 | $headers = $GLOBALS['TTDF_CONFIG']['REST_API']['HEADERS'] ?? []; 154 | 155 | // 动态设置允许的来源 156 | $requestOrigin = $_SERVER['HTTP_ORIGIN'] ?? ''; 157 | $allowedOrigins = [$requestOrigin, $_SERVER['HTTP_HOST'] ?? '']; 158 | $headers['Access-Control-Allow-Origin'] = in_array($requestOrigin, $allowedOrigins, true) 159 | ? $requestOrigin 160 | : ($allowedOrigins[1] ?? '*'); 161 | 162 | // 添加必要的CORS头 163 | $headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization, X-Requested-With'; 164 | $headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'; 165 | $headers['Access-Control-Allow-Credentials'] = 'true'; 166 | 167 | // 防止缓存 168 | $headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'; 169 | $headers['Pragma'] = 'no-cache'; 170 | $headers['Expires'] = '0'; 171 | 172 | foreach ($headers as $name => $value) { 173 | if (!headers_sent() && $value !== null) { 174 | header("$name: $value"); 175 | } 176 | } 177 | 178 | if (defined('__DEBUG__') && __DEBUG__) { 179 | TTDF_DebugLogger::logApiProcess('SECURITY_HEADERS_SET'); 180 | } 181 | } catch (Throwable $e) { 182 | if (defined('__DEBUG__') && __DEBUG__) { 183 | TTDF_DebugLogger::logApiError('Error in setSecurityHeaders', $e); 184 | } 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * 专门负责格式化数据 191 | */ 192 | final class ApiFormatter 193 | { 194 | public function __construct( 195 | private readonly TTDF_Db_API $dbApi, 196 | private readonly ContentFormat $contentFormat, 197 | private readonly int $excerptLength 198 | ) {} 199 | 200 | public function formatPost(array $post): array 201 | { 202 | $formattedPost = [ 203 | 'cid' => (int)($post['cid'] ?? 0), 204 | 'title' => $post['title'] ?? '', 205 | 'slug' => $post['slug'] ?? '', 206 | 'type' => $post['type'] ?? 'post', 207 | 'created' => date('c', $post['created'] ?? time()), 208 | 'modified' => date('c', $post['modified'] ?? time()), 209 | 'commentsNum' => (int)($post['commentsNum'] ?? 0), 210 | 'authorId' => (int)($post['authorId'] ?? 0), 211 | 'status' => $post['status'] ?? 'publish', 212 | 'contentType' => $this->contentFormat->value, 213 | 'fields' => $this->dbApi->getPostFields($post['cid'] ?? 0), 214 | 'content' => $this->formatContent($post['text'] ?? ''), 215 | 'excerpt' => $this->generatePlainExcerpt($post['text'] ?? '', $this->excerptLength), 216 | ]; 217 | 218 | if ($formattedPost['type'] === 'post') { 219 | $formattedPost['categories'] = array_map( 220 | [$this, 'formatCategory'], 221 | $this->dbApi->getPostCategories($post['cid'] ?? 0) 222 | ); 223 | $formattedPost['tags'] = array_map( 224 | [$this, 'formatTag'], 225 | $this->dbApi->getPostTags($post['cid'] ?? 0) 226 | ); 227 | } 228 | return $formattedPost; 229 | } 230 | 231 | public function formatCategory(array $category): array 232 | { 233 | $category['description'] = $this->formatContent($category['description'] ?? ''); 234 | return $category; 235 | } 236 | 237 | public function formatTag(array $tag): array 238 | { 239 | $tag['description'] = $this->formatContent($tag['description'] ?? ''); 240 | return $tag; 241 | } 242 | 243 | public function formatComment(array $comment): array 244 | { 245 | return [ 246 | 'coid' => (int)($comment['coid'] ?? 0), 247 | 'cid' => (int)($comment['cid'] ?? 0), 248 | 'author' => $comment['author'] ?? '', 249 | 'mail' => md5($comment['mail'] ?? ''), 250 | 'url' => $comment['url'] ?? '', 251 | // 'ip' => $comment['ip'] ?? '', 252 | 'created' => date('c', $comment['created'] ?? time()), 253 | 'modified' => date('c', $comment['modified'] ?? time()), 254 | 'text' => $this->formatContent($comment['text'] ?? ''), 255 | 'status' => $comment['status'] ?? 'approved', 256 | 'parent' => (int)($comment['parent'] ?? 0), 257 | 'authorId' => (int)($comment['authorId'] ?? 0) 258 | ]; 259 | } 260 | 261 | public function formatAttachment(array $attachment): array 262 | { 263 | return [ 264 | 'cid' => (int)($attachment['cid'] ?? 0), 265 | 'title' => $attachment['title'] ?? '', 266 | 'type' => $attachment['type'] ?? '', 267 | 'size' => (int)($attachment['size'] ?? 0), 268 | 'created' => date('c', $attachment['created'] ?? time()), 269 | 'modified' => date('c', $attachment['modified'] ?? time()), 270 | 'status' => $attachment['status'] ?? 'publish', 271 | ]; 272 | } 273 | 274 | private function formatContent(string $content): string 275 | { 276 | if ($this->contentFormat === ContentFormat::MARKDOWN) { 277 | return $content; 278 | } 279 | if (!class_exists('Markdown')) { 280 | require_once __TYPECHO_ROOT_DIR__ . '/var/Typecho/Common/Markdown.php'; 281 | } 282 | return Markdown::convert(preg_replace('//s', '', $content)); 283 | } 284 | 285 | private function generatePlainExcerpt(string $content, int $length): string 286 | { 287 | if ($length <= 0) { 288 | return ''; 289 | } 290 | // 移除HTML和Markdown 291 | $text = strip_tags($content); 292 | $text = preg_replace([ 293 | '/```.*?```/s', 294 | '/~~~.*?~~~/s', 295 | '/\[[^\]]*\]\([^\)]*\)/', // 修复链接正则表达式 296 | '/!\[[^\]]*\]\([^\)]*\)/', // 修复图片正则表达式 297 | '/\[([^\]]*)\]\([^\)]*\)/', // 修复链接文本正则表达式 298 | '/^#{1,6}\s*/m', 299 | '/[\*\_]{1,3}/', 300 | '/^\s*>\s*/m', 301 | '/\s+/' 302 | ], ' ', $text); 303 | $text = trim($text ?? ''); // 修复可能为null的问题 304 | 305 | if (mb_strlen($text) > $length) { 306 | $text = mb_substr($text, 0, $length); 307 | // 避免截断在单词中间 308 | if (preg_match('/^(.*)\s\S*$/u', $text, $matches)) { 309 | $text = $matches[1]; 310 | } 311 | } 312 | return $text; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /app/setup.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'title' => '欢迎使用', 11 | 'html' => [ 12 | [ 13 | // 'Content' => '自定义输出HTML内容', 14 | 'content' => ' 15 | 20 | 24 | 25 | 26 | 27 | 28 | GitHub 29 | 30 | 31 | BiliBili 32 | 33 | 34 | Releases 35 | 36 | 37 | 38 | ' 39 | ], 40 | ] 41 | ], 42 | 'Demo' => [ 43 | 'title' => '组件演示', 44 | 'fields' => [ 45 | [ 46 | // 'Html' => '自定义HTML标签', 47 | 'type' => 'Html', 48 | 'content' => '
感谢使用TTDF进行开发
' 49 | ], 50 | [ 51 | // 'Text' => '文本框', 52 | 'type' => 'Text', 53 | 'name' => 'SubTitle', 54 | 'value' => '', 55 | 'label' => '副标题', 56 | 'description' => '这是一个文本框,用于设置网站副标题,如果为空则不显示。' 57 | ], 58 | [ 59 | // 'Textarea' => '文本域', 60 | 'type' => 'Textarea', 61 | 'name' => 'this_textarea', 62 | 'value' => '', 63 | 'label' => '文本域', 64 | 'description' => '这是一个文本域~' 65 | ], 66 | [ 67 | // 'AddList' => '动态列表', 68 | 'type' => 'AddList', 69 | 'name' => 'this_addlist', 70 | 'value' => '项目1,项目2,项目3', 71 | 'label' => '动态列表测试', 72 | 'description' => '这是一个AddList组件,点击+1按钮可以添加新的输入框,数据以逗号分隔存储。' 73 | ], 74 | [ 75 | // 'Switch' => '开关切换', 76 | 'type' => 'Switch', 77 | 'name' => 'demo_switch', 78 | 'value' => true, 79 | 'label' => '开关切换', 80 | 'description' => '开关组件,用于切换布尔值状态', 81 | 'active_text' => '开启', 82 | 'inactive_text' => '关闭' 83 | ], 84 | [ 85 | // 'Radio' => '单选框', 86 | 'type' => 'Radio', 87 | 'name' => 'this_radio', 88 | 'value' => 'option1', 89 | 'label' => '单选框', 90 | 'description' => '这是一个单选框~', 91 | 'options' => [ 92 | 'option1' => '选项一', 93 | 'option2' => '选项二', 94 | 'option3' => '选项三' 95 | ] 96 | ], 97 | [ 98 | // 'Select' => '下拉框', 99 | 'type' => 'Select', 100 | 'name' => 'this_select', 101 | 'value' => 'option2', 102 | 'label' => '下拉框', 103 | 'description' => '这是一个下拉框~', 104 | 'options' => [ 105 | 'option1' => '选项一', 106 | 'option2' => '选项二', 107 | 'option3' => '选项三' 108 | ] 109 | ], 110 | [ 111 | // 'Checkbox' => '多选框', 112 | 'type' => 'Checkbox', 113 | 'name' => 'this_checkbox', 114 | 'value' => ['option1', 'option3'], 115 | 'label' => '多选框', 116 | 'description' => '这是一个多选框~', 117 | 'options' => [ 118 | 'option1' => '选项一', 119 | 'option2' => '选项二', 120 | 'option3' => '选项三' 121 | ] 122 | ], 123 | [ 124 | // 'ColorPicker' => '颜色选择器', 125 | 'type' => 'ColorPicker', 126 | 'name' => 'theme_color', 127 | 'value' => '#3498db', 128 | 'label' => '主题颜色', 129 | 'description' => '选择网站的主题颜色,支持十六进制颜色值输入。' 130 | ], 131 | [ 132 | // 'DatePicker' => '日期选择器', 133 | 'type' => 'DatePicker', 134 | 'name' => 'demo_date', 135 | 'value' => '', 136 | 'label' => '日期选择', 137 | 'description' => '选择一个日期,格式为 YYYY-MM-DD', 138 | 'format' => 'YYYY-MM-DD', 139 | 'placeholder' => '请选择日期' 140 | ], 141 | [ 142 | // 'TimePicker' => '时间选择器', 143 | 'type' => 'TimePicker', 144 | 'name' => 'demo_time', 145 | 'value' => '', 146 | 'label' => '时间选择', 147 | 'description' => '选择一个时间,格式为 HH:mm:ss', 148 | 'format' => 'HH:mm:ss', 149 | 'placeholder' => '请选择时间' 150 | ], 151 | [ 152 | // 'Number' => '数字输入框', 153 | 'type' => 'Number', 154 | 'name' => 'demo_number', 155 | 'value' => 100, 156 | 'label' => '数字输入', 157 | 'description' => '数字输入框,支持设置最小值、最大值和步长', 158 | 'min' => 0, 159 | 'max' => 1000, 160 | 'step' => 10, 161 | 'placeholder' => '请输入数字' 162 | ], 163 | [ 164 | // 'Slider' => '滑块', 165 | 'type' => 'Slider', 166 | 'name' => 'demo_slider', 167 | 'value' => 50, 168 | 'label' => '滑块控制', 169 | 'description' => '滑块组件,用于在指定范围内选择数值', 170 | 'min' => 0, 171 | 'max' => 100, 172 | 'step' => 5, 173 | 'show_stops' => true 174 | ], 175 | [ 176 | // 'Code' => '代码编辑器', 177 | 'type' => 'Code', 178 | 'name' => 'demo_code', 179 | 'value' => '// JavaScript代码示例\nfunction hello() {\n console.log("Hello World!");\n}', 180 | 'label' => '代码编辑', 181 | 'description' => '代码编辑器,支持语法高亮和代码格式化', 182 | 'language' => 'javascript', 183 | 'theme' => 'vs-dark' 184 | ], 185 | [ 186 | // 'Tags' => '标签输入', 187 | 'type' => 'Tags', 188 | 'name' => 'demo_tags', 189 | 'value' => 'Vue,JavaScript,CSS', 190 | 'label' => '标签输入', 191 | 'description' => '标签输入组件,支持动态添加和删除标签', 192 | 'placeholder' => '输入标签后按回车', 193 | 'max_tags' => 10 194 | ], 195 | [ 196 | // 'Cascader' => '级联选择器', 197 | 'type' => 'Cascader', 198 | 'name' => 'demo_cascader', 199 | 'value' => 'frontend,vue,vue3', 200 | 'label' => '级联选择', 201 | 'description' => '级联选择器,支持多级分类选择', 202 | 'placeholder' => '请选择分类', 203 | 'options' => [ 204 | [ 205 | 'value' => 'frontend', 206 | 'label' => '前端开发', 207 | 'children' => [ 208 | [ 209 | 'value' => 'vue', 210 | 'label' => 'Vue.js', 211 | 'children' => [ 212 | ['value' => 'vue2', 'label' => 'Vue 2.x'], 213 | ['value' => 'vue3', 'label' => 'Vue 3.x'] 214 | ] 215 | ], 216 | [ 217 | 'value' => 'react', 218 | 'label' => 'React', 219 | 'children' => [ 220 | ['value' => 'react16', 'label' => 'React 16'], 221 | ['value' => 'react17', 'label' => 'React 17'], 222 | ['value' => 'react18', 'label' => 'React 18'] 223 | ] 224 | ] 225 | ] 226 | ], 227 | [ 228 | 'value' => 'backend', 229 | 'label' => '后端开发', 230 | 'children' => [ 231 | [ 232 | 'value' => 'nodejs', 233 | 'label' => 'Node.js', 234 | 'children' => [ 235 | ['value' => 'express', 'label' => 'Express'], 236 | ['value' => 'koa', 'label' => 'Koa'] 237 | ] 238 | ], 239 | [ 240 | 'value' => 'php', 241 | 'label' => 'PHP', 242 | 'children' => [ 243 | ['value' => 'laravel', 'label' => 'Laravel'], 244 | ['value' => 'symfony', 'label' => 'Symfony'] 245 | ] 246 | ] 247 | ] 248 | ] 249 | ] 250 | ], 251 | [ 252 | // 'Transfer' => '穿梭框', 253 | 'type' => 'Transfer', 254 | 'name' => 'demo_transfer', 255 | 'value' => 'item1,item3', 256 | 'label' => '穿梭框', 257 | 'description' => '穿梭框组件,用于在两个列表之间移动选项', 258 | 'titles' => ['可选项', '已选项'], 259 | 'button_texts' => ['移除', '添加'], 260 | 'data' => [ 261 | ['key' => 'item1', 'label' => '选项 1', 'disabled' => false], 262 | ['key' => 'item2', 'label' => '选项 2', 'disabled' => false], 263 | ['key' => 'item3', 'label' => '选项 3', 'disabled' => false], 264 | ['key' => 'item4', 'label' => '选项 4', 'disabled' => false], 265 | ['key' => 'item5', 'label' => '选项 5', 'disabled' => false] 266 | ] 267 | ] 268 | ] 269 | ], 270 | 'TTDF-Options' => [ 271 | 'title' => '其他设置', 272 | 'fields' => [ 273 | [ 274 | 'type' => 'Html', 275 | 'content' => '
如果关闭将无法使用RestAPI
' 276 | ], 277 | [ 278 | 'type' => 'Select', 279 | 'name' => 'RESTAPI_Switch', 280 | 'value' => 'false', 281 | 'label' => 'REST API', 282 | 'description' => 'TTDF框架内置的 REST API
使用教程可参见 *这里*', 283 | 'options' => [ 284 | 'true' => '开启', 285 | 'false' => '关闭' 286 | ] 287 | ], 288 | ] 289 | ], 290 | ]; 291 | -------------------------------------------------------------------------------- /core/Main.php: -------------------------------------------------------------------------------- 1 | TTDF_ConfigManager::get('app.debug', false), 35 | 'FIELDS_ENABLED' => TTDF_ConfigManager::get('app.fields.enabled', false), 36 | 'TYAJAX_ENABLED' => TTDF_ConfigManager::get('plugins.tyajax.enabled', false), 37 | 'COMPRESS_HTML' => TTDF_ConfigManager::get('app.compress_html', false), 38 | 'GRAVATAR_PREFIX' => TTDF_ConfigManager::get('modules.gravatar.prefix', 'https://cravatar.cn/avatar/'), 39 | 40 | // REST_API 相关配置 41 | 'REST_API' => [ 42 | 'ENABLED' => TTDF_ConfigManager::get('modules.restapi.enabled', false), 43 | 'ROUTE' => TTDF_ConfigManager::get('modules.restapi.route', 'ty-json'), 44 | 'OVERRIDE_SETTING' => TTDF_ConfigManager::get('modules.restapi.override_setting', 'RESTAPI_Switch'), 45 | 'TOKEN' => TTDF_ConfigManager::get('modules.restapi.token.value', '1778273540'), 46 | 'LIMIT' => TTDF_ConfigManager::get('modules.restapi.limit', []), 47 | 'HEADERS' => TTDF_ConfigManager::get('modules.restapi.headers', []), 48 | ], 49 | ]; 50 | 51 | // 定义 TTDF_CONFIG 常量 52 | define('TTDF_CONFIG', $TTDF_CONFIG_ARRAY); 53 | 54 | /** 55 | * TTDF 配置管理器 56 | * 支持不区分大小写的配置访问和向后兼容的配置键映射 57 | */ 58 | class TTDF_ConfigManager 59 | { 60 | /** @var array 配置数据 */ 61 | private static $config = []; 62 | 63 | /** @var array 配置键映射(小写 => 原始键) */ 64 | private static $keyMap = []; 65 | 66 | /** @var array 配置值缓存 */ 67 | private static $cache = []; 68 | 69 | /** @var bool 是否已初始化 */ 70 | private static $initialized = false; 71 | 72 | /** @var array 老配置键到新配置路径的映射 */ 73 | private static $legacyKeyMap = [ 74 | 'DEBUG' => 'app.debug', 75 | 'FIELDS_ENABLED' => 'app.fields.enabled', 76 | 'TYAJAX_ENABLED' => 'plugins.tyajax.enabled', 77 | 'COMPRESS_HTML' => 'app.compress_html', 78 | 'GRAVATAR_PREFIX' => 'modules.gravatar.prefix', 79 | 'REST_API' => 'modules.restapi', 80 | ]; 81 | 82 | /** 83 | * 初始化配置管理器 84 | * 85 | * @param array $config 配置数组 86 | * @throws RuntimeException 如果重复初始化 87 | */ 88 | public static function init(array $config): void 89 | { 90 | if (self::$initialized) { 91 | throw new RuntimeException('配置管理器已经初始化,不能重复初始化'); 92 | } 93 | 94 | self::$config = $config; 95 | self::buildKeyMap($config); 96 | self::$initialized = true; 97 | } 98 | 99 | /** 100 | * 构建键映射 101 | * 102 | * @param array $config 配置数组 103 | * @param string $prefix 前缀 104 | */ 105 | private static function buildKeyMap(array $config, string $prefix = '') 106 | { 107 | foreach ($config as $key => $value) { 108 | $fullKey = $prefix ? $prefix . '.' . $key : $key; 109 | $lowerKey = strtolower($fullKey); 110 | self::$keyMap[$lowerKey] = $fullKey; 111 | 112 | if (is_array($value)) { 113 | self::buildKeyMap($value, $fullKey); 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * 获取配置值 120 | * 121 | * @param string $key 配置键(支持点号分隔,不区分大小写) 122 | * @param mixed $default 默认值 123 | * @return mixed 124 | */ 125 | public static function get(string $key, $default = null) 126 | { 127 | // 生成缓存键 128 | $cacheKey = $key . '::' . serialize($default); 129 | 130 | // 检查缓存 131 | if (isset(self::$cache[$cacheKey])) { 132 | return self::$cache[$cacheKey]; 133 | } 134 | 135 | $lowerKey = strtolower($key); 136 | $result = null; 137 | 138 | // 检查是否有映射的键 139 | if (isset(self::$keyMap[$lowerKey])) { 140 | $actualKey = self::$keyMap[$lowerKey]; 141 | $result = self::getNestedValue(self::$config, $actualKey, $default); 142 | } else { 143 | // 直接尝试获取(兼容原有方式) 144 | $result = self::getNestedValue(self::$config, $key, $default); 145 | } 146 | 147 | // 缓存结果 148 | self::$cache[$cacheKey] = $result; 149 | 150 | return $result; 151 | } 152 | 153 | /** 154 | * 获取老配置键对应的值(向后兼容) 155 | * 156 | * @param string $legacyKey 老配置键 157 | * @return mixed 158 | */ 159 | public static function getLegacyKey(string $legacyKey) 160 | { 161 | // 检查是否有映射 162 | if (isset(self::$legacyKeyMap[$legacyKey])) { 163 | $newKey = self::$legacyKeyMap[$legacyKey]; 164 | return self::get($newKey); 165 | } 166 | 167 | // 特殊处理 REST_API 子键 168 | if (strpos($legacyKey, 'REST_API.') === 0) { 169 | $subKey = substr($legacyKey, 9); // 移除 'REST_API.' 前缀 170 | $keyMap = [ 171 | 'ENABLED' => 'modules.restapi.enabled', 172 | 'ROUTE' => 'modules.restapi.route', 173 | 'OVERRIDE_SETTING' => 'modules.restapi.override_setting', 174 | 'TOKEN' => 'modules.restapi.token', 175 | 'LIMIT' => 'modules.restapi.limit', 176 | 'HEADERS' => 'modules.restapi.headers', 177 | ]; 178 | 179 | if (isset($keyMap[$subKey])) { 180 | return self::get($keyMap[$subKey]); 181 | } 182 | } 183 | 184 | return null; 185 | } 186 | 187 | /** 188 | * 检查老配置键是否存在 189 | * 190 | * @param string $legacyKey 老配置键 191 | * @return bool 192 | */ 193 | public static function hasLegacyKey(string $legacyKey): bool 194 | { 195 | return self::getLegacyKey($legacyKey) !== null; 196 | } 197 | 198 | /** 199 | * 获取嵌套配置值 200 | * 201 | * @param array $config 配置数组 202 | * @param string $key 配置键 203 | * @param mixed $default 默认值 204 | * @return mixed 205 | */ 206 | private static function getNestedValue(array $config, string $key, $default = null) 207 | { 208 | $keys = explode('.', $key); 209 | $value = $config; 210 | 211 | foreach ($keys as $k) { 212 | if (!is_array($value) || !isset($value[$k])) { 213 | return $default; 214 | } 215 | $value = $value[$k]; 216 | } 217 | 218 | return $value; 219 | } 220 | 221 | /** 222 | * 检查配置是否存在 223 | * 224 | * @param string $key 配置键 225 | * @return bool 226 | */ 227 | public static function has(string $key): bool 228 | { 229 | $lowerKey = strtolower($key); 230 | return isset(self::$keyMap[$lowerKey]) || self::getNestedValue(self::$config, $key) !== null; 231 | } 232 | 233 | /** 234 | * 获取所有配置 235 | * 236 | * @return array 237 | */ 238 | public static function all(): array 239 | { 240 | return self::$config; 241 | } 242 | 243 | /** 244 | * 清除配置缓存 245 | * 246 | * @return void 247 | */ 248 | public static function clearCache(): void 249 | { 250 | self::$cache = []; 251 | } 252 | 253 | /** 254 | * 获取缓存统计信息 255 | * 256 | * @return array 257 | */ 258 | public static function getCacheStats(): array 259 | { 260 | return [ 261 | 'cache_size' => count(self::$cache), 262 | 'initialized' => self::$initialized, 263 | 'config_keys' => count(self::$keyMap) 264 | ]; 265 | } 266 | } 267 | 268 | /** 269 | * 全局配置访问函数 270 | * 271 | * @param string $key 配置键(支持点号分隔,不区分大小写) 272 | * @param mixed $default 默认值 273 | * @return mixed 274 | */ 275 | function config(string $key, $default = null) 276 | { 277 | return TTDF_ConfigManager::get($key, $default); 278 | } 279 | 280 | // ErrorHandler trait 已移至 Modules/ErrorHandler.php 文件中 281 | 282 | /** 283 | * 单例Widget Trait 284 | */ 285 | trait SingletonWidget 286 | { 287 | /** @var Widget\Archive|null */ 288 | private static $widget; 289 | 290 | private static function getArchive() 291 | { 292 | if (self::$widget === null) { 293 | try { 294 | self::$widget = \Widget\Archive::widget('Widget_Archive'); 295 | } catch (Exception $e) { 296 | throw new RuntimeException('初始化Widget失败: ' . $e->getMessage(), 0, $e); 297 | } 298 | } 299 | return self::$widget; 300 | } 301 | } 302 | 303 | class TTDF_Main 304 | { 305 | /** @var array 已加载模块 */ 306 | private static $loadedModules = []; 307 | 308 | /** 309 | * 运行框架 310 | */ 311 | public static function run() 312 | { 313 | // 加载核心模块 314 | self::loadCoreModules(); 315 | 316 | // 加载Widgets 317 | self::loadWidgets(); 318 | 319 | // 加载可选模块 320 | self::loadOptionalModules(); 321 | 322 | // 配置检查 323 | if (!defined('TTDF_CONFIG')) { 324 | throw new RuntimeException('TTDF配置未初始化'); 325 | } 326 | 327 | // 初始化数据库 328 | TTDF_Db::init(); 329 | } 330 | 331 | /** 332 | * 加载核心模块 333 | */ 334 | private static function loadCoreModules() 335 | { 336 | require_once __DIR__ . '/Modules/ErrorHandler.php'; 337 | require_once __DIR__ . '/Modules/Database.php'; 338 | require_once __DIR__ . '/Modules/CacheManager.php'; 339 | if (config('app.debug', false)) { 340 | require_once __DIR__ . '/Modules/Debug.php'; 341 | } 342 | } 343 | 344 | /** 345 | * 加载Widgets 346 | */ 347 | private static function loadWidgets() 348 | { 349 | $widgetFiles = [ 350 | 'Tools.php', 351 | 'Hook.php', 352 | 'TTDF.php', 353 | 'AddRoute.php', 354 | 'OOP/Common.php', 355 | 'OOP/Site.php', 356 | 'OOP/Post.php', 357 | 'OOP/Theme.php', 358 | 'OOP/User.php', 359 | 'OOP/Comment.php', 360 | ]; 361 | 362 | foreach ($widgetFiles as $file) { 363 | require_once __DIR__ . '/Widget/' . $file; 364 | } 365 | } 366 | 367 | /** 368 | * 加载可选模块 369 | */ 370 | private static function loadOptionalModules() 371 | { 372 | $moduleFiles = [ 373 | 'OPP.php', 374 | 'Api.php', 375 | 'Options.php', 376 | 'RouterAuto.php', 377 | ]; 378 | 379 | foreach ($moduleFiles as $file) { 380 | require_once __DIR__ . '/Modules/' . $file; 381 | } 382 | 383 | if (config('plugins.tyajax.enabled', false)) { 384 | require_once __DIR__ . '/Widget/TyAjax.php'; 385 | } 386 | } 387 | 388 | /** 389 | * 初始化框架 390 | */ 391 | public static function init() 392 | { 393 | // 运行框架 394 | self::run(); 395 | 396 | // HTML压缩 397 | if (config('app.compress_html', false)) { 398 | ob_start(function ($buffer) { 399 | return TTDF::compressHtml($buffer); 400 | }); 401 | } 402 | } 403 | } 404 | 405 | // 只有在 Typecho 环境下才初始化框架 406 | if (defined('__TYPECHO_ROOT_DIR__') && !defined('TTDF_TEST_MODE')) { 407 | // 初始化框架 408 | try { 409 | TTDF_Main::init(); 410 | 411 | // 初始化错误处理系统 412 | $errorHandler = TTDF_ErrorHandler::getInstance(); 413 | $errorHandler->init([ 414 | 'debug' => config('app.debug', false), 415 | 'log_file' => dirname(__DIR__) . '/logs/error.log' 416 | ]); 417 | } catch (Exception $e) { 418 | // 框架初始化失败 419 | $errorHandler = TTDF_ErrorHandler::getInstance(); 420 | $errorHandler->fatal('Framework initialization failed', [], $e); 421 | 422 | if (config('app.debug', false)) { 423 | throw $e; 424 | } 425 | error_log('Framework init error: ' . $e->getMessage()); 426 | exit('系统初始化失败'); 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /core/Widget/Tools.php: -------------------------------------------------------------------------------- 1 | init(); 35 | } 36 | } 37 | 38 | /** 39 | * 清理缓存 40 | */ 41 | public static function clearCache(): void 42 | { 43 | self::$reflectionCache = []; 44 | } 45 | 46 | /** 47 | * 获取缓存统计 48 | */ 49 | public static function getCacheStats(): array 50 | { 51 | return [ 52 | 'size' => count(self::$reflectionCache), 53 | 'max_size' => self::MAX_CACHE_SIZE, 54 | 'keys' => array_keys(self::$reflectionCache) 55 | ]; 56 | } 57 | /** 58 | * 获取类的详细反射信息并格式化输出 59 | * 通过反射机制获取指定函数的全面详细属性和元数据,支持缓存 60 | * 61 | * @param string|object $class 类名或类对象 62 | * @param bool $returnArray 是否返回数组(默认为false,直接输出) 63 | * @param bool $useCache 是否使用缓存(默认为true) 64 | * @return array|void 类信息数组(当$returnArray为true时) 65 | * @throws \ReflectionException 66 | */ 67 | public static function ClassDetails($class, ?bool $returnArray = false, bool $useCache = true) 68 | { 69 | try { 70 | // 生成缓存键 71 | $className = is_object($class) ? get_class($class) : $class; 72 | $cacheKey = md5($className . '_' . ($returnArray ? '1' : '0')); 73 | 74 | // 检查缓存 75 | if ($useCache && isset(self::$reflectionCache[$cacheKey])) { 76 | $classInfo = self::$reflectionCache[$cacheKey]; 77 | 78 | if (!$returnArray) { 79 | self::outputClassInfo($classInfo); 80 | return; 81 | } 82 | return $classInfo; 83 | } 84 | 85 | $reflector = new \ReflectionClass($class); 86 | 87 | // 安全地获取默认值的函数 88 | $getSafeDefaultValue = function ($value) { 89 | try { 90 | if (is_object($value)) { 91 | return get_class($value); 92 | } 93 | if (is_array($value)) { 94 | return array_map(function ($item) { 95 | return is_object($item) ? get_class($item) : $item; 96 | }, $value); 97 | } 98 | return $value; 99 | } catch (\Throwable $e) { 100 | return '无法获取默认值'; 101 | } 102 | }; 103 | 104 | // 递归获取父类继承链 105 | $getParentChain = function ($reflector) use (&$getParentChain) { 106 | $parentChain = []; 107 | $currentParent = $reflector->getParentClass(); 108 | 109 | while ($currentParent) { 110 | $parentChain[] = [ 111 | 'className' => $currentParent->getName(), 112 | 'namespace' => $currentParent->getNamespaceName(), 113 | 'shortName' => $currentParent->getShortName() 114 | ]; 115 | $currentParent = $currentParent->getParentClass(); 116 | } 117 | 118 | return $parentChain; 119 | }; 120 | 121 | // 基本类信息 122 | $namespace = $reflector->getNamespaceName(); 123 | $className = $reflector->getName(); 124 | $shortClassName = $reflector->getShortName(); 125 | 126 | // 获取完整父类继承链 127 | $parentChain = $getParentChain($reflector); 128 | 129 | // 接口信息 130 | $interfaces = $reflector->getInterfaceNames(); 131 | 132 | // 属性信息 133 | $properties = array_map(function ($prop) use ($getSafeDefaultValue) { 134 | try { 135 | return [ 136 | 'name' => $prop->getName(), 137 | 'type' => $prop->getType() ? $prop->getType()->getName() : 'mixed', 138 | // 替换 match 表达式兼容php7 139 | 'visibility' => (function () use ($prop) { 140 | if ($prop->isPublic()) { 141 | return 'public'; 142 | } elseif ($prop->isProtected()) { 143 | return 'protected'; 144 | } elseif ($prop->isPrivate()) { 145 | return 'private'; 146 | } else { 147 | return 'unknown'; 148 | } 149 | })(), 150 | 'static' => $prop->isStatic(), 151 | 'hasDefaultValue' => $prop->hasDefaultValue(), 152 | 'defaultValue' => $prop->hasDefaultValue() 153 | ? $getSafeDefaultValue($prop->getDefaultValue()) 154 | : null 155 | ]; 156 | } catch (\Throwable $e) { 157 | return [ 158 | 'name' => $prop->getName(), 159 | 'error' => '无法获取属性详情:' . $e->getMessage() 160 | ]; 161 | } 162 | }, $reflector->getProperties()); 163 | 164 | // 方法信息 165 | $methods = array_map(function ($method) use ($getSafeDefaultValue) { 166 | try { 167 | return [ 168 | 'name' => $method->getName(), 169 | // 替换 match 表达式兼容php7 170 | 'visibility' => (function () use ($method) { 171 | if ($method->isPublic()) { 172 | return 'public'; 173 | } elseif ($method->isProtected()) { 174 | return 'protected'; 175 | } elseif ($method->isPrivate()) { 176 | return 'private'; 177 | } else { 178 | return 'unknown'; 179 | } 180 | })(), 181 | 'static' => $method->isStatic(), 182 | 'abstract' => $method->isAbstract(), 183 | 'final' => $method->isFinal(), 184 | 'parameters' => array_map(function ($param) use ($getSafeDefaultValue) { 185 | return [ 186 | 'name' => $param->getName(), 187 | 'type' => $param->hasType() ? $param->getType()->getName() : 'mixed', 188 | 'optional' => $param->isOptional() ?? false, 189 | 'defaultValue' => $param->isOptional() 190 | ? ($param->isDefaultValueAvailable() 191 | ? $getSafeDefaultValue($param->getDefaultValue()) 192 | : null) 193 | : null 194 | ]; 195 | }, $method->getParameters()) 196 | ]; 197 | } catch (\Throwable $e) { 198 | return [ 199 | 'name' => $method->getName(), 200 | 'error' => '无法获取方法详情:' . $e->getMessage() 201 | ]; 202 | } 203 | }, $reflector->getMethods()); 204 | 205 | // 准备返回的数组 206 | $classInfo = [ 207 | 'fullClassName' => $className, 208 | 'shortClassName' => $shortClassName, 209 | 'namespace' => $namespace, 210 | 'parentChain' => $parentChain, 211 | 'interfaces' => $interfaces, 212 | 'properties' => $properties, 213 | 'methods' => $methods, 214 | 'isAbstract' => $reflector->isAbstract(), 215 | 'isFinal' => $reflector->isFinal(), 216 | 'isInterface' => $reflector->isInterface(), 217 | 'isTrait' => $reflector->isTrait(), 218 | 'fileName' => $reflector->getFileName(), 219 | 'constants' => $reflector->getConstants() 220 | ]; 221 | 222 | // 缓存结果 223 | if ($useCache) { 224 | // 如果缓存已满,清理最老的条目 225 | if (count(self::$reflectionCache) >= self::MAX_CACHE_SIZE) { 226 | $oldestKey = array_key_first(self::$reflectionCache); 227 | unset(self::$reflectionCache[$oldestKey]); 228 | } 229 | self::$reflectionCache[$cacheKey] = $classInfo; 230 | } 231 | 232 | // 根据参数决定返回或输出 233 | if ($returnArray) { 234 | return $classInfo; 235 | } 236 | 237 | self::outputClassInfo($classInfo); 238 | } catch (\ReflectionException $e) { 239 | self::$errorHandler->error('反射错误: ' . $e->getMessage(), ['class' => $className], $e); 240 | if (!$returnArray) { 241 | echo "反射错误: " . $e->getMessage() . "\n"; 242 | } 243 | } catch (\Throwable $e) { 244 | self::$errorHandler->error('ClassDetails未知错误: ' . $e->getMessage(), ['class' => $className], $e); 245 | if (!$returnArray) { 246 | echo "未知错误: " . $e->getMessage() . "\n"; 247 | } 248 | } 249 | 250 | if ($returnArray) { 251 | return $classInfo ?? []; 252 | } 253 | } 254 | 255 | /** 256 | * 输出类信息的私有方法 257 | * 258 | * @param array $classInfo 类信息数组 259 | */ 260 | private static function outputClassInfo(array $classInfo): void 261 | { 262 | // 文本输出 263 | echo "完整类名: {$classInfo['fullClassName']}\n"; 264 | echo "短类名: {$classInfo['shortClassName']}\n"; 265 | echo "命名空间: {$classInfo['namespace']}\n"; 266 | echo "文件位置: " . ($classInfo['fileName'] ?: '未知') . "\n"; 267 | 268 | // 输出父类继承链 269 | echo "父类继承链: \n"; 270 | if (empty($classInfo['parentChain'])) { 271 | echo " 无父类\n"; 272 | } else { 273 | foreach ($classInfo['parentChain'] as $index => $parent) { 274 | echo " " . str_repeat("└── ", $index) . 275 | "父类 " . ($index + 1) . ": {$parent['className']} (命名空间: {$parent['namespace']})\n"; 276 | } 277 | } 278 | 279 | // 输出接口信息 280 | echo "实现的接口: \n"; 281 | if (empty($classInfo['interfaces'])) { 282 | echo " 无接口\n"; 283 | } else { 284 | foreach ($classInfo['interfaces'] as $interface) { 285 | echo " - {$interface}\n"; 286 | } 287 | } 288 | 289 | // 输出常量信息 290 | echo "类常量: \n"; 291 | if (empty($classInfo['constants'])) { 292 | echo " 无常量\n"; 293 | } else { 294 | foreach ($classInfo['constants'] as $name => $value) { 295 | echo " - {$name}: " . (is_array($value) ? json_encode($value) : $value) . "\n"; 296 | } 297 | } 298 | 299 | // 输出属性信息 300 | echo "类属性: \n"; 301 | if (empty($classInfo['properties'])) { 302 | echo " 无属性\n"; 303 | } else { 304 | foreach ($classInfo['properties'] as $prop) { 305 | if (isset($prop['error'])) { 306 | echo " - 错误: {$prop['error']}\n"; 307 | continue; 308 | } 309 | 310 | $defaultValue = $prop['hasDefaultValue'] 311 | ? ' (默认值: ' . (is_array($prop['defaultValue']) ? json_encode($prop['defaultValue']) : $prop['defaultValue']) . ')' 312 | : ''; 313 | echo " - {$prop['visibility']} " . ($prop['static'] ? 'static ' : '') . 314 | "{$prop['type']} \${$prop['name']}{$defaultValue}\n"; 315 | } 316 | } 317 | 318 | // 输出方法信息 319 | echo "类方法: \n"; 320 | if (empty($classInfo['methods'])) { 321 | echo " 无方法\n"; 322 | } else { 323 | foreach ($classInfo['methods'] as $method) { 324 | if (isset($method['error'])) { 325 | echo " - 错误: {$method['error']}\n"; 326 | continue; 327 | } 328 | 329 | $params = array_map(function ($param) { 330 | $defaultValue = $param['optional'] && $param['defaultValue'] !== null 331 | ? ' = ' . (is_array($param['defaultValue']) ? json_encode($param['defaultValue']) : $param['defaultValue']) 332 | : ''; 333 | return "{$param['type']} \${$param['name']}{$defaultValue}"; 334 | }, $method['parameters']); 335 | 336 | $modifiers = []; 337 | if ($method['abstract']) $modifiers[] = 'abstract'; 338 | if ($method['final']) $modifiers[] = 'final'; 339 | if ($method['static']) $modifiers[] = 'static'; 340 | 341 | $modifierStr = !empty($modifiers) ? implode(' ', $modifiers) . ' ' : ''; 342 | 343 | echo " - {$method['visibility']} {$modifierStr}{$method['name']}(" . implode(', ', $params) . ")\n"; 344 | } 345 | } 346 | 347 | // 额外类型信息 348 | echo "\n类型信息:\n"; 349 | echo " 抽象类: " . ($classInfo['isAbstract'] ? '是' : '否') . "\n"; 350 | echo " Final类: " . ($classInfo['isFinal'] ? '是' : '否') . "\n"; 351 | echo " 接口: " . ($classInfo['isInterface'] ? '是' : '否') . "\n"; 352 | echo " Trait: " . ($classInfo['isTrait'] ? '是' : '否') . "\n"; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /core/Widget/Hook.php: -------------------------------------------------------------------------------- 1 | 'header', 20 | 'Widget_Archive_footer' => 'footer', 21 | 'Widget_Archive_head' => 'load_head', 22 | 'Widget_Archive_foot' => 'load_foot', 23 | 'Widget_Archive_beforeRender' => 'before_render', 24 | 'Widget_Archive_afterRender' => 'after_render', 25 | 'Widget_Contents_Post_beforeRender' => 'post_before_render', 26 | 'Widget_Contents_Post_afterRender' => 'post_after_render', 27 | 'Widget_Contents_Page_beforeRender' => 'page_before_render', 28 | 'Widget_Contents_Page_afterRender' => 'page_after_render', 29 | ]; 30 | 31 | /** 32 | * 调试日志记录 33 | */ 34 | private static function debug_log($message, $data = null) 35 | { 36 | if (!self::$debug_enabled) { 37 | return; 38 | } 39 | 40 | if (self::$debug_log_file === null) { 41 | self::$debug_log_file = __DIR__ . '/../../logs/hook.log'; 42 | } 43 | 44 | $timestamp = date('Y-m-d H:i:s.') . substr(microtime(), 2, 3); 45 | $log_entry = "[{$timestamp}] {$message}"; 46 | 47 | if ($data !== null) { 48 | $log_entry .= " | Data: " . json_encode($data, JSON_UNESCAPED_UNICODE); 49 | } 50 | 51 | $log_entry .= "\n"; 52 | 53 | file_put_contents(self::$debug_log_file, $log_entry, FILE_APPEND | LOCK_EX); 54 | } 55 | 56 | /** 57 | * 初始化钩子系统 58 | */ 59 | private static function init() 60 | { 61 | self::debug_log("Hook system initializing"); 62 | 63 | if (!self::$buffer_started) { 64 | ob_start(); 65 | self::$buffer_started = true; 66 | self::debug_log("Output buffer started"); 67 | } 68 | 69 | if (!self::$shutdown_registered) { 70 | register_shutdown_function([__CLASS__, 'finalize']); 71 | self::$shutdown_registered = true; 72 | self::debug_log("Shutdown function registered"); 73 | } 74 | 75 | // 注册 Typecho 钩子监听 76 | self::registerTypechoHooks(); 77 | } 78 | 79 | /** 80 | * 注册 Typecho 钩子监听 81 | */ 82 | private static function registerTypechoHooks() 83 | { 84 | if (self::$typecho_hooks_registered) { 85 | return; 86 | } 87 | 88 | // 检查 Typecho_Plugin 类是否存在 89 | if (!class_exists('Typecho_Plugin')) { 90 | return; 91 | } 92 | 93 | // 为每个映射的 Typecho 钩子注册监听器 94 | foreach (self::$typecho_hook_mapping as $typecho_hook => $ttdf_hook) { 95 | try { 96 | // 使用正确的 Typecho 钩子注册方式 97 | Typecho_Plugin::factory($typecho_hook)->trigger = function() use ($ttdf_hook) { 98 | // 获取传递给钩子的参数 99 | $args = func_get_args(); 100 | 101 | // 触发对应的 TTDF 钩子 102 | self::do_action($ttdf_hook, $args); 103 | }; 104 | } catch (Exception $e) { 105 | // 忽略不存在的钩子 106 | continue; 107 | } 108 | } 109 | 110 | self::$typecho_hooks_registered = true; 111 | } 112 | 113 | /** 114 | * 注册钩子 115 | * @param string $hook_name 钩子名称 116 | * @param callable $callback 回调函数 117 | * @param int $priority 优先级(数字越小优先级越高) 118 | */ 119 | public static function add_action($hook_name, $callback, $priority = 10) 120 | { 121 | self::init(); 122 | 123 | $callback_info = is_string($callback) ? $callback : (is_array($callback) ? implode('::', $callback) : 'closure'); 124 | $is_already_executed = isset(self::$executed_hooks[$hook_name]); 125 | self::debug_log("Registering hook: {$hook_name}", [ 126 | 'callback' => $callback_info, 127 | 'priority' => $priority, 128 | 'already_executed' => $is_already_executed, 129 | 'current_actions_count' => isset(self::$actions[$hook_name]) ? count(self::$actions[$hook_name], COUNT_RECURSIVE) - count(self::$actions[$hook_name]) : 0 130 | ]); 131 | 132 | if (!isset(self::$actions[$hook_name])) { 133 | self::$actions[$hook_name] = []; 134 | } 135 | 136 | if (!isset(self::$actions[$hook_name][$priority])) { 137 | self::$actions[$hook_name][$priority] = []; 138 | } 139 | 140 | self::$actions[$hook_name][$priority][] = $callback; 141 | 142 | // 如果钩子已经执行过,收集新注册回调的内容 143 | if ($is_already_executed) { 144 | self::debug_log("Hook {$hook_name} already executed, collecting late-registered callback content"); 145 | ob_start(); 146 | call_user_func($callback, self::$executed_hooks[$hook_name]); 147 | $content = ob_get_clean(); 148 | 149 | // 将内容添加到对应钩子的内容收集中 150 | if (!isset(self::$hook_contents[$hook_name])) { 151 | self::$hook_contents[$hook_name] = ''; 152 | } 153 | self::$hook_contents[$hook_name] .= $content; 154 | 155 | self::debug_log("Late-registered callback content collected", [ 156 | 'hook_name' => $hook_name, 157 | 'content_length' => strlen($content), 158 | 'total_content_length' => strlen(self::$hook_contents[$hook_name]) 159 | ]); 160 | } 161 | } 162 | 163 | /** 164 | * 执行钩子 165 | * @param string $hook_name 钩子名称 166 | * @param mixed $args 传递给回调函数的参数 167 | * @param bool $return_content 是否返回内容而不是直接输出 168 | */ 169 | public static function do_action($hook_name, $args = null, $return_content = false) 170 | { 171 | self::init(); 172 | 173 | $registered_callbacks_count = 0; 174 | if (isset(self::$actions[$hook_name])) { 175 | foreach (self::$actions[$hook_name] as $priority => $callbacks) { 176 | $registered_callbacks_count += count($callbacks); 177 | } 178 | } 179 | 180 | // 记录执行参数 181 | $was_already_executed = isset(self::$executed_hooks[$hook_name]); 182 | self::$executed_hooks[$hook_name] = $args; 183 | 184 | self::debug_log("Executing hook: {$hook_name}", [ 185 | 'args' => $args, 186 | 'return_content' => $return_content, 187 | 'registered_callbacks_count' => $registered_callbacks_count, 188 | 'already_executed' => $was_already_executed 189 | ]); 190 | 191 | // 收集当前已注册的钩子内容 192 | ob_start(); 193 | $executed_callbacks = 0; 194 | if (isset(self::$actions[$hook_name])) { 195 | ksort(self::$actions[$hook_name]); 196 | foreach (self::$actions[$hook_name] as $priority => $callbacks) { 197 | foreach ($callbacks as $callback) { 198 | $callback_info = is_string($callback) ? $callback : (is_array($callback) ? implode('::', $callback) : 'closure'); 199 | self::debug_log("Executing callback for {$hook_name}", [ 200 | 'callback' => $callback_info, 201 | 'priority' => $priority 202 | ]); 203 | call_user_func($callback, $args); 204 | $executed_callbacks++; 205 | } 206 | } 207 | } 208 | $content = ob_get_clean(); 209 | 210 | self::debug_log("Hook execution completed: {$hook_name}", [ 211 | 'executed_callbacks' => $executed_callbacks, 212 | 'content_length' => strlen($content) 213 | ]); 214 | 215 | // 初始化钩子内容收集 216 | if (!isset(self::$hook_contents[$hook_name])) { 217 | self::$hook_contents[$hook_name] = ''; 218 | } 219 | self::$hook_contents[$hook_name] .= $content; 220 | 221 | if ($return_content) { 222 | self::debug_log("Returning content for {$hook_name}", [ 223 | 'total_content_length' => strlen(self::$hook_contents[$hook_name]) 224 | ]); 225 | return self::$hook_contents[$hook_name]; 226 | } else { 227 | // 生成唯一占位符 228 | $placeholder = ''; 229 | self::$hook_placeholders[$placeholder] = $hook_name; 230 | self::debug_log("Generated placeholder for {$hook_name}", [ 231 | 'placeholder' => $placeholder, 232 | 'total_placeholders' => count(self::$hook_placeholders) 233 | ]); 234 | echo $placeholder; 235 | } 236 | } 237 | 238 | /** 239 | * 获取钩子内容(不执行,只返回) 240 | * @param string $hook_name 钩子名称 241 | * @param mixed $args 传递给回调函数的参数 242 | * @return string 243 | */ 244 | public static function get_hook_content($hook_name, $args = null) 245 | { 246 | return self::do_action($hook_name, $args, true); 247 | } 248 | 249 | /** 250 | * 检查钩子是否已执行 251 | * @param string $hook_name 钩子名称 252 | * @return bool 253 | */ 254 | public static function has_executed($hook_name) 255 | { 256 | return isset(self::$executed_hooks[$hook_name]); 257 | } 258 | 259 | /** 260 | * 移除钩子 261 | * @param string $hook_name 钩子名称 262 | * @param callable $callback 回调函数 263 | * @param int $priority 优先级 264 | */ 265 | public static function remove_action($hook_name, $callback, $priority = 10) 266 | { 267 | if (isset(self::$actions[$hook_name][$priority])) { 268 | $key = array_search($callback, self::$actions[$hook_name][$priority], true); 269 | if ($key !== false) { 270 | unset(self::$actions[$hook_name][$priority][$key]); 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * 自动在脚本结束时调用 277 | */ 278 | public static function finalize() 279 | { 280 | self::debug_log("Finalize started", [ 281 | 'buffer_started' => self::$buffer_started, 282 | 'ob_level' => ob_get_level(), 283 | 'total_placeholders' => count(self::$hook_placeholders), 284 | 'executed_hooks' => array_keys(self::$executed_hooks) 285 | ]); 286 | 287 | if (self::$buffer_started && ob_get_level() > 0) { 288 | $output = ob_get_clean(); 289 | self::debug_log("Output buffer cleaned", ['output_length' => strlen($output)]); 290 | 291 | // 在最终输出前,再次执行所有已注册但可能在占位符生成后才注册的钩子 292 | foreach (self::$hook_placeholders as $placeholder => $hook_name) { 293 | $current_callbacks_count = 0; 294 | if (isset(self::$actions[$hook_name])) { 295 | foreach (self::$actions[$hook_name] as $priority => $callbacks) { 296 | $current_callbacks_count += count($callbacks); 297 | } 298 | } 299 | 300 | self::debug_log("Processing placeholder: {$placeholder}", [ 301 | 'hook_name' => $hook_name, 302 | 'current_callbacks_count' => $current_callbacks_count, 303 | 'has_executed_hooks' => isset(self::$executed_hooks[$hook_name]), 304 | 'current_content_length' => isset(self::$hook_contents[$hook_name]) ? strlen(self::$hook_contents[$hook_name]) : 0 305 | ]); 306 | 307 | // 确保钩子内容是最新的 308 | if (isset(self::$actions[$hook_name]) && isset(self::$executed_hooks[$hook_name])) { 309 | // 重新收集钩子内容,确保包含所有后注册的回调 310 | ob_start(); 311 | $fresh_executed_callbacks = 0; 312 | ksort(self::$actions[$hook_name]); 313 | foreach (self::$actions[$hook_name] as $priority => $callbacks) { 314 | foreach ($callbacks as $callback) { 315 | $callback_info = is_string($callback) ? $callback : (is_array($callback) ? implode('::', $callback) : 'closure'); 316 | self::debug_log("Re-executing callback in finalize", [ 317 | 'hook_name' => $hook_name, 318 | 'callback' => $callback_info, 319 | 'priority' => $priority 320 | ]); 321 | call_user_func($callback, self::$executed_hooks[$hook_name]); 322 | $fresh_executed_callbacks++; 323 | } 324 | } 325 | $fresh_content = ob_get_clean(); 326 | 327 | self::debug_log("Fresh content collected in finalize", [ 328 | 'hook_name' => $hook_name, 329 | 'fresh_executed_callbacks' => $fresh_executed_callbacks, 330 | 'fresh_content_length' => strlen($fresh_content), 331 | 'old_content_length' => isset(self::$hook_contents[$hook_name]) ? strlen(self::$hook_contents[$hook_name]) : 0 332 | ]); 333 | 334 | // 更新钩子内容 335 | self::$hook_contents[$hook_name] = $fresh_content; 336 | } 337 | } 338 | 339 | // 替换所有占位符为实际内容 340 | foreach (self::$hook_placeholders as $placeholder => $hook_name) { 341 | $replacement_content = ''; 342 | if (isset(self::$hook_contents[$hook_name])) { 343 | $replacement_content = self::$hook_contents[$hook_name]; 344 | } 345 | 346 | self::debug_log("Replacing placeholder", [ 347 | 'placeholder' => $placeholder, 348 | 'hook_name' => $hook_name, 349 | 'replacement_length' => strlen($replacement_content), 350 | 'found_in_output' => strpos($output, $placeholder) !== false 351 | ]); 352 | 353 | $output = str_replace($placeholder, $replacement_content, $output); 354 | } 355 | 356 | self::debug_log("Final output prepared", [ 357 | 'final_output_length' => strlen($output), 358 | 'placeholders_processed' => count(self::$hook_placeholders) 359 | ]); 360 | 361 | echo $output; 362 | self::$buffer_started = false; 363 | } 364 | 365 | self::debug_log("Finalize completed"); 366 | } 367 | 368 | /** 369 | * 清理钩子数据 370 | */ 371 | public static function clear() 372 | { 373 | self::debug_log("Clearing hook data"); 374 | 375 | self::$actions = []; 376 | self::$hook_placeholders = []; 377 | self::$hook_contents = []; 378 | self::$executed_hooks = []; 379 | self::$placeholder_counter = 0; 380 | 381 | if (self::$buffer_started && ob_get_level() > 0) { 382 | ob_end_clean(); 383 | self::$buffer_started = false; 384 | } 385 | 386 | self::debug_log("Hook data cleared"); 387 | } 388 | 389 | /** 390 | * 启用或禁用调试日志 391 | */ 392 | public static function set_debug($enabled = true) 393 | { 394 | self::$debug_enabled = $enabled; 395 | if ($enabled) { 396 | self::debug_log("Debug logging enabled"); 397 | } 398 | } 399 | 400 | /** 401 | * 获取调试日志文件路径 402 | */ 403 | public static function get_debug_log_file() 404 | { 405 | if (self::$debug_log_file === null) { 406 | self::$debug_log_file = __DIR__ . '/../../logs/hook.log'; 407 | } 408 | return self::$debug_log_file; 409 | } 410 | } 411 | 412 | -------------------------------------------------------------------------------- /core/Widget/OOP/Post.php: -------------------------------------------------------------------------------- 1 | init(); 49 | } 50 | 51 | // 初始化缓存管理器 52 | if (class_exists('TTDF_CacheManager')) { 53 | TTDF_CacheManager::init(); 54 | } 55 | } 56 | 57 | /** 58 | * 获取当前文章实例 59 | * 如果 `_currentArchive` 为空,则调用 `getArchive` 方法初始化 60 | * @return Typecho_Widget 61 | */ 62 | public static function getCurrentArchive() // 修改 protected -> public 63 | { 64 | return self::$_currentArchive ?? self::getArchive(); 65 | } 66 | 67 | /** 68 | * 绑定当前文章实例 69 | * 70 | * @param Typecho_Widget $archive 文章实例 71 | */ 72 | public static function bindArchive($archive) 73 | { 74 | self::$_currentArchive = $archive; 75 | } 76 | 77 | /** 78 | * 解除当前文章实例的绑定 79 | * 将 `_currentArchive` 设置为 null,释放资源 80 | */ 81 | public static function unbindArchive() 82 | { 83 | self::$_currentArchive = null; 84 | } 85 | 86 | /** 87 | * 文章列表获取 88 | * 支持自定义查询参数或默认获取下一篇文章 89 | * 90 | * @param array|null $params 查询参数 91 | * @return Typecho_Widget 返回文章实例或空对象 92 | */ 93 | public static function List($params = null) 94 | { 95 | try { 96 | self::initErrorHandler(); 97 | 98 | if ($params) { 99 | // 生成缓存键 100 | $cacheKey = 'list_' . md5(serialize($params)); 101 | 102 | // 检查缓存 103 | $cachedWidget = TTDF_CacheManager::get($cacheKey); 104 | if ($cachedWidget !== null) { 105 | return $cachedWidget; 106 | } 107 | 108 | $alias = 'custom_' . md5(serialize($params)); 109 | $widget = \Widget\Archive::allocWithAlias( 110 | $alias, 111 | is_array($params) ? http_build_query($params) : $params 112 | ); 113 | $widget->execute(); 114 | self::$_currentArchive = $widget; 115 | 116 | // 缓存结果 117 | TTDF_CacheManager::set($cacheKey, $widget, 300); // 缓存5分钟 118 | return $widget; 119 | } 120 | 121 | if (method_exists(self::getArchive(), 'Next')) { 122 | return self::getArchive()->Next(); 123 | } 124 | throw new Exception('List 方法不存在'); 125 | } catch (Exception $e) { 126 | self::$errorHandler->error('List 调用失败', ['params' => $params], $e); 127 | return new \Typecho_Widget_Helper_Empty(); 128 | } 129 | } 130 | 131 | /** 132 | * 获取随机文章列表 133 | * 134 | * @param int $pageSize 随机文章数量 135 | * @return array 返回随机文章列表 136 | */ 137 | public static function RandomPosts(int $pageSize = 3): array 138 | { 139 | try { 140 | self::initErrorHandler(); 141 | 142 | // 检查缓存 143 | $cacheKey = "random_posts_{$pageSize}"; 144 | $posts = TTDF_CacheManager::get($cacheKey); 145 | 146 | if ($posts === null) { 147 | $posts = TTDF_Db::getInstance()->getRandomPosts($pageSize); 148 | TTDF_CacheManager::set($cacheKey, $posts, 600); // 缓存10分钟 149 | } 150 | 151 | return $posts; 152 | } catch (Exception $e) { 153 | self::$errorHandler->error('获取随机文章失败', ['pageSize' => $pageSize], $e); 154 | return []; 155 | } 156 | } 157 | 158 | /** 159 | * 渲染随机文章列表 160 | * 161 | * @param int $pageSize 随机文章数量 162 | * @param bool $echo 是否直接输出,默认为 true 163 | * @return array 164 | */ 165 | public static function RenderRandomPosts(int $pageSize = 3, bool $echo = true): array 166 | { 167 | try { 168 | self::initErrorHandler(); 169 | $posts = self::RandomPosts($pageSize); 170 | 171 | if ($echo && !empty($posts)) { 172 | foreach ($posts as $post) { 173 | $title = htmlspecialchars($post['title'] ?? '', ENT_QUOTES, 'UTF-8'); 174 | $permalink = htmlspecialchars($post['permalink'] ?? '', ENT_QUOTES, 'UTF-8'); 175 | echo '' . $title . '
'; 176 | } 177 | } 178 | 179 | return $posts; 180 | } catch (Exception $e) { 181 | self::$errorHandler->error('渲染随机文章失败', ['pageSize' => $pageSize, 'echo' => $echo], $e); 182 | return []; 183 | } 184 | } 185 | 186 | // 数据获取方法 187 | 188 | /** 189 | * 获取文章CID 190 | * 191 | * @param bool $echo 是否直接输出,默认为 true 192 | * @return int 返回文章CID 193 | */ 194 | public static function Cid(bool $echo = true): int 195 | { 196 | try { 197 | self::initErrorHandler(); 198 | $archive = self::getCurrentArchive(); 199 | $cid = $archive->cid ?? 0; 200 | 201 | if ($echo) { 202 | echo $cid; 203 | } 204 | 205 | return $cid; 206 | } catch (Exception $e) { 207 | self::$errorHandler->error('获取Cid失败', ['echo' => $echo], $e); 208 | return 0; 209 | } 210 | } 211 | 212 | /** 213 | * 获取文章标题 214 | * 215 | * @param bool $echo 是否直接输出,默认为 true 216 | * @return string 返回标题字符串 217 | */ 218 | public static function Title(bool $echo = true): string 219 | { 220 | try { 221 | self::initErrorHandler(); 222 | $archive = self::getCurrentArchive(); 223 | $title = $archive->title ?? ''; 224 | 225 | if ($echo) { 226 | echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); 227 | } 228 | 229 | return $title; 230 | } catch (Exception $e) { 231 | self::$errorHandler->error('获取标题失败', ['echo' => $echo], $e); 232 | return ''; 233 | } 234 | } 235 | 236 | /** 237 | * 获取文章日期 238 | * 239 | * @param string $format 日期格式,默认为 'Y-m-d' 240 | * @param bool $echo 是否直接输出,默认为 true 241 | * @return string|null 返回日期字符串或直接输出 242 | */ 243 | public static function Date($format = 'Y-m-d', $echo = true) 244 | { 245 | try { 246 | $date = self::getCurrentArchive()->date($format); 247 | return self::outputValue($date, $echo); 248 | } catch (Exception $e) { 249 | return self::handleOutputError('获取日期失败', $e, $echo, ''); 250 | } 251 | } 252 | 253 | /** 254 | * 获取文章分类 255 | * 256 | * @param string $split 分隔符,默认为 ',' 257 | * @param bool $link 是否生成链接,默认为 true 258 | * @param string $default 默认值,默认为 '暂无分类' 259 | * @param bool $echo 是否直接输出,默认为 true 260 | * @return string|null 返回分类字符串或直接输出 261 | */ 262 | public static function Category($split = ',', $link = true, $default = '暂无分类', $echo = true) 263 | { 264 | try { 265 | $category = self::getCurrentArchive()->category($split, $link, $default); 266 | return self::outputValue($category, $echo); 267 | } catch (Exception $e) { 268 | return self::handleOutputError('获取分类失败', $e, $echo, $default); 269 | } 270 | } 271 | 272 | /** 273 | * 获取文章标签 274 | * 275 | * @param string $split 分隔符,默认为 ',' 276 | * @param bool $link 是否生成链接,默认为 true 277 | * @param string $default 默认值,默认为 '暂无标签' 278 | * @param bool $echo 是否直接输出,默认为 true 279 | * @return string|null 返回标签字符串或直接输出 280 | */ 281 | public static function Tags($split = ',', $link = true, $default = '暂无标签', $echo = true) 282 | { 283 | try { 284 | $tags = self::getCurrentArchive()->tags($split, $link, $default); 285 | return self::outputValue($tags, $echo); 286 | } catch (Exception $e) { 287 | return self::handleOutputError('获取标签失败', $e, $echo, $default); 288 | } 289 | } 290 | 291 | /** 292 | * 获取文章摘要 293 | * 294 | * @param int $length 摘要长度,0 表示不限制 295 | * @param bool $echo 是否直接输出,默认为 true 296 | * @return string|null 返回摘要字符串或直接输出 297 | */ 298 | public static function Excerpt($length = 0, $echo = true) 299 | { 300 | try { 301 | $excerpt = strip_tags(self::getCurrentArchive()->excerpt); 302 | $excerpt = $length > 0 ? mb_substr($excerpt, 0, $length, 'UTF-8') : $excerpt; 303 | return self::outputValue($excerpt, $echo); 304 | } catch (Exception $e) { 305 | return self::handleOutputError('获取摘要失败', $e, $echo); 306 | } 307 | } 308 | 309 | /** 310 | * 获取文章永久链接 311 | * 312 | * @param bool $echo 是否直接输出,默认为 true 313 | * @return string|null 返回链接字符串或直接输出 314 | */ 315 | public static function Permalink($echo = true) 316 | { 317 | try { 318 | $permalink = self::getCurrentArchive()->permalink; 319 | return self::outputValue($permalink, $echo); 320 | } catch (Exception $e) { 321 | return self::handleOutputError('获取链接失败', $e, $echo); 322 | } 323 | } 324 | 325 | /** 326 | * 获取文章内容 327 | * 328 | * @param bool $echo 是否直接输出,默认为 true 329 | * @return string|null 返回内容字符串或直接输出 330 | */ 331 | public static function Content($echo = true) 332 | { 333 | try { 334 | $content = self::getCurrentArchive()->content; 335 | return self::outputValue($content, $echo); 336 | } catch (Exception $e) { 337 | return self::handleOutputError('获取内容失败', $e, $echo); 338 | } 339 | } 340 | 341 | /** 342 | * 获取归档标题 343 | * 344 | * @param string $format 格式化字符串,默认为空 345 | * @param string $default 默认值,默认为空 346 | * @param string $connector 连接符,默认为空 347 | * @param bool $echo 是否直接输出,默认为 true 348 | * @return string|null 返回标题字符串或直接输出 349 | */ 350 | public static function ArchiveTitle($format = '', $default = '', $connector = '', $echo = true) 351 | { 352 | try { 353 | $title = empty($format) 354 | ? self::getCurrentArchive()->archiveTitle 355 | : self::getCurrentArchive()->archiveTitle($format, $default, $connector); 356 | return self::outputValue($title, $echo); 357 | } catch (Exception $e) { 358 | return self::handleOutputError('获取标题失败', $e, $echo); 359 | } 360 | } 361 | 362 | /** 363 | * 获取文章作者名称 364 | * 365 | * @param bool $echo 是否直接输出,默认为 true 366 | * @return string|null 返回作者名称或直接输出 367 | */ 368 | public static function Author($echo = true) 369 | { 370 | try { 371 | $author = self::getCurrentArchive()->author->screenName; 372 | return self::outputValue($author, $echo); 373 | } catch (Exception $e) { 374 | return self::handleOutputError('获取作者失败', $e, $echo); 375 | } 376 | } 377 | 378 | /** 379 | * 获取文章作者头像 380 | * 381 | * @param int $size 头像尺寸,默认为 128 382 | * @param bool $echo 是否直接输出,默认为 true 383 | * @return string|null 返回头像 URL 或直接输出 384 | */ 385 | public static function AuthorAvatar($size = 128, $echo = true) 386 | { 387 | try { 388 | $avatar = self::getCurrentArchive()->author->gravatar($size); 389 | return self::outputValue($avatar, $echo); 390 | } catch (Exception $e) { 391 | return self::handleOutputError('获取头像失败', $e, $echo); 392 | } 393 | } 394 | 395 | /** 396 | * 获取文章作者链接 397 | * 398 | * @param bool $echo 是否直接输出,默认为 true 399 | * @return string|null 返回作者链接或直接输出 400 | */ 401 | public static function AuthorPermalink($echo = true) 402 | { 403 | try { 404 | $link = self::getCurrentArchive()->author->permalink; 405 | return self::outputValue($link, $echo); 406 | } catch (Exception $e) { 407 | return self::handleOutputError('获取作者链接失败', $e, $echo); 408 | } 409 | } 410 | 411 | /** 412 | * 统计文章字数 413 | * 414 | * @param bool $echo 是否直接输出,默认为 true 415 | * @return int|null 返回字数或直接输出 416 | */ 417 | public static function WordCount($echo = true) 418 | { 419 | try { 420 | $cid = self::getCurrentArchive()->cid; 421 | $text = TTDF_Db::getInstance()->getArticleText($cid); 422 | $text = preg_replace("/[^\x{4e00}-\x{9fa5}]/u", "", $text); 423 | $count = mb_strlen($text, 'UTF-8'); 424 | return self::outputValue($count, $echo); 425 | } catch (Exception $e) { 426 | return self::handleOutputError('统计字数失败', $e, $echo); 427 | } 428 | } 429 | 430 | /** 431 | * 获取文章总数 432 | * 433 | * @param bool $echo 是否直接输出,默认为 true 434 | * @return int|null 返回文章总数或直接输出 435 | */ 436 | public static function PostsNum($echo = true) 437 | { 438 | try { 439 | $count = TTDF_Db::getInstance()->getArticleCount(); 440 | return self::outputValue($count, $echo); 441 | } catch (Exception $e) { 442 | return self::handleOutputError('获取文章数失败', $e, $echo); 443 | } 444 | } 445 | 446 | /** 447 | * 从数据库获取文章标题 448 | * 449 | * @param bool $echo 是否直接输出,默认为 true 450 | * @return string|null 返回标题字符串或直接输出 451 | */ 452 | public static function DB_Title($echo = true) 453 | { 454 | try { 455 | $title = TTDF_Db::getInstance()->getArticleTitle(self::getCurrentArchive()->cid); 456 | return self::outputValue($title, $echo); 457 | } catch (Exception $e) { 458 | return self::handleOutputError('获取数据库标题失败', $e, $echo); 459 | } 460 | } 461 | 462 | /** 463 | * 从数据库获取文章内容 464 | * 465 | * @param bool $echo 是否直接输出,默认为 true 466 | * @return string|null 返回内容字符串或直接输出 467 | */ 468 | public static function DB_Content($echo = true) 469 | { 470 | try { 471 | $content = TTDF_Db::getInstance()->getArticleContent(self::getCurrentArchive()->cid); 472 | return self::outputValue($content, $echo); 473 | } catch (Exception $e) { 474 | return self::handleOutputError('获取数据库内容失败', $e, $echo); 475 | } 476 | } 477 | 478 | /** 479 | * 从数据库获取文章内容并转换为 HTML 480 | * 481 | * @param bool $echo 是否直接输出,默认为 true 482 | * @return string|null 返回 HTML 内容或直接输出 483 | */ 484 | public static function DB_Content_Html($echo = true) 485 | { 486 | try { 487 | $content = TTDF_Db::getInstance()->getArticleContent(self::getCurrentArchive()->cid); 488 | $content = preg_replace('//', '', $content); // 移除注释避免干扰markdown解析 489 | $html = Markdown::convert($content); 490 | return self::outputValue($html, $echo); 491 | } catch (Exception $e) { 492 | return self::handleOutputError('转换HTML失败', $e, $echo); 493 | } 494 | } 495 | 496 | /** 497 | * 统一输出处理方法 498 | * 499 | * @param mixed $value 输出值 500 | * @param bool $echo 是否直接输出 501 | * @return mixed 返回值或直接输出 502 | */ 503 | private static function outputValue($value, $echo) 504 | { 505 | if ($echo) { 506 | echo $value; 507 | return null; 508 | } 509 | return $value; 510 | } 511 | 512 | /** 513 | * 统一错误处理方法 514 | * 515 | * @param string $message 错误信息 516 | * @param Exception $exception 异常对象 517 | * @param bool $echo 是否直接输出 518 | * @param mixed $default 默认返回值 519 | * @return mixed 返回默认值或直接输出 520 | */ 521 | private static function handleOutputError($message, $exception, $echo, $default = '') 522 | { 523 | self::handleError($message, $exception); 524 | return self::outputValue($default, $echo); 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /core/Static/Options.css: -------------------------------------------------------------------------------- 1 | /** 2 | * TTDF主题设置项 3 | * @author 鼠子Tomoriゞ 4 | */ 5 | 6 | /* Typecho CSS 重置部分 */ 7 | .typecho-foot { 8 | display: none; 9 | } 10 | 11 | .typecho-head-nav .operate a { 12 | background-color: #202328; 13 | } 14 | 15 | .typecho-option-tabs li { 16 | float: left; 17 | background-color: #fffbcc; 18 | } 19 | 20 | .typecho-page-main .typecho-option textarea { 21 | height: 150px; 22 | } 23 | 24 | .typecho-option-submit li { 25 | display: none; 26 | } 27 | 28 | .row [class*="col-"] { 29 | float: unset; 30 | min-height: unset; 31 | padding-right: unset; 32 | padding-left: unset; 33 | } 34 | 35 | @media (min-width: 768px) { 36 | .col-tb-offset-2 { 37 | margin-left: unset; 38 | } 39 | 40 | .col-tb-8 { 41 | flex: unset; 42 | max-width: unset; 43 | } 44 | } 45 | 46 | .col-mb-12 { 47 | width: unset; 48 | } 49 | 50 | /* TTDF 基础样式 */ 51 | 52 | /* DialogSelect 对话框 */ 53 | .ttdf-dialog { 54 | border-radius: 12px; 55 | box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); 56 | border: none; 57 | } 58 | 59 | .ttdf-dialog .el-dialog__header { 60 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 61 | border-bottom: none; 62 | border-radius: 12px 12px 0 0; 63 | padding: 20px 24px; 64 | } 65 | 66 | .ttdf-dialog .el-dialog__title { 67 | font-size: 18px; 68 | font-weight: 600; 69 | color: white; 70 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 71 | } 72 | 73 | .ttdf-dialog .el-dialog__headerbtn .el-dialog__close { 74 | color: rgba(255, 255, 255, 0.8); 75 | font-size: 20px; 76 | transition: all 0.3s ease; 77 | } 78 | 79 | .ttdf-dialog .el-dialog__headerbtn .el-dialog__close:hover { 80 | color: white; 81 | transform: rotate(90deg); 82 | } 83 | 84 | .ttdf-dialog .el-dialog__body { 85 | padding: 24px; 86 | background: white; 87 | max-height: 65vh; 88 | overflow-y: auto; 89 | } 90 | 91 | .dialog-content { 92 | min-height: 200px; 93 | } 94 | 95 | .option-container { 96 | width: 100%; 97 | } 98 | 99 | .dialog-search { 100 | margin-bottom: 20px; 101 | } 102 | 103 | .dialog-search .el-input__wrapper { 104 | border-radius: 8px; 105 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); 106 | } 107 | 108 | .option-group { 109 | width: 100%; 110 | display: grid; 111 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 112 | gap: 12px; 113 | } 114 | 115 | .option-item { 116 | background: #fafbfc; 117 | border: 2px solid #e1e5e9; 118 | border-radius: 8px; 119 | padding: 16px; 120 | transition: all 0.3s ease; 121 | cursor: pointer; 122 | position: relative; 123 | overflow: hidden; 124 | } 125 | 126 | .option-item::before { 127 | content: ''; 128 | position: absolute; 129 | top: 0; 130 | left: 0; 131 | width: 100%; 132 | height: 3px; 133 | background: linear-gradient(90deg, #667eea, #764ba2); 134 | transform: scaleX(0); 135 | transition: transform 0.3s ease; 136 | } 137 | 138 | .option-item:hover { 139 | border-color: #667eea; 140 | background: #f0f4ff; 141 | transform: translateY(-2px); 142 | box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15); 143 | } 144 | 145 | .option-item:hover::before { 146 | transform: scaleX(1); 147 | } 148 | 149 | .option-checkbox, 150 | .option-radio { 151 | width: 100%; 152 | margin: 0; 153 | } 154 | 155 | .option-checkbox .el-checkbox__label, 156 | .option-radio .el-radio__label { 157 | width: 100%; 158 | padding-left: 12px; 159 | font-size: 14px; 160 | color: #2c3e50; 161 | font-weight: 500; 162 | line-height: 1.5; 163 | } 164 | 165 | .ttdf-dialog .el-dialog__footer { 166 | background: #f8f9fa; 167 | border-top: 1px solid #e9ecef; 168 | border-radius: 0 0 12px 12px; 169 | padding: 16px 24px; 170 | } 171 | 172 | .dialog-footer { 173 | display: flex; 174 | justify-content: flex-end; 175 | gap: 12px; 176 | } 177 | 178 | .cancel-btn { 179 | background: #6c757d; 180 | border-color: #6c757d; 181 | color: white; 182 | border-radius: 6px; 183 | padding: 10px 20px; 184 | font-size: 14px; 185 | font-weight: 500; 186 | transition: all 0.3s ease; 187 | } 188 | 189 | .cancel-btn:hover { 190 | background: #5a6268; 191 | border-color: #5a6268; 192 | transform: translateY(-1px); 193 | } 194 | 195 | .confirm-btn { 196 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 197 | border: none; 198 | border-radius: 6px; 199 | padding: 10px 20px; 200 | font-size: 14px; 201 | font-weight: 500; 202 | transition: all 0.3s ease; 203 | } 204 | 205 | .confirm-btn:hover { 206 | transform: translateY(-1px); 207 | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); 208 | } 209 | 210 | /* 选中状态样式 */ 211 | .option-item:has(.el-checkbox__input.is-checked), 212 | .option-item:has(.el-radio__input.is-checked) { 213 | border-color: #667eea; 214 | background: linear-gradient(135deg, #f0f4ff 0%, #e8f2ff 100%); 215 | box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2); 216 | } 217 | 218 | .option-item:has(.el-checkbox__input.is-checked)::before, 219 | .option-item:has(.el-radio__input.is-checked)::before { 220 | transform: scaleX(1); 221 | } 222 | 223 | .no-results { 224 | text-align: center; 225 | color: #6c757d; 226 | padding: 40px 20px; 227 | font-size: 16px; 228 | background: #f8f9fa; 229 | border-radius: 8px; 230 | margin: 20px 0; 231 | } 232 | 233 | /* Tags 输入框样式 */ 234 | .tags-input-container { 235 | width: 100%; 236 | } 237 | 238 | .tags-display { 239 | margin-bottom: 12px; 240 | min-height: 32px; 241 | padding: 8px; 242 | border: 1px solid #dcdfe6; 243 | border-radius: 6px; 244 | background: #fafbfc; 245 | } 246 | 247 | .tags-display:empty::before { 248 | content: '暂无标签'; 249 | color: #c0c4cc; 250 | font-size: 14px; 251 | } 252 | 253 | * { 254 | margin: 0; 255 | padding: 0; 256 | box-sizing: border-box; 257 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 258 | Oxygen-Sans, Ubuntu, Cantarell, sans-serif; 259 | } 260 | 261 | /* 内容标题样式 */ 262 | .TTDF-content h1 { 263 | padding: 0px 6px; 264 | border-left: 10px solid #ed515191; 265 | background-color: rgba(208, 208, 208, 0); 266 | font-size: 19px; 267 | line-height: 30px; 268 | color: cornflowerblue; 269 | font-weight: bold; 270 | margin: 16px 0; 271 | } 272 | 273 | .TTDF-content h2 { 274 | padding: 0px 6px; 275 | border-left: 10px solid #BF51ED91; 276 | background-color: rgba(208, 208, 208, 0); 277 | font-size: 18px; 278 | line-height: 30px; 279 | color: cornflowerblue; 280 | font-weight: bold; 281 | margin: 16px 0; 282 | } 283 | 284 | .TTDF-content h3 { 285 | padding: 0px 6px; 286 | border-left: 10px solid #6495ed91; 287 | background-color: rgba(208, 208, 208, 0); 288 | font-size: 17px; 289 | line-height: 27px; 290 | color: cornflowerblue; 291 | font-weight: bold; 292 | margin: 16px 0; 293 | } 294 | 295 | .TTDF-content h4 { 296 | padding: 0px 6px; 297 | border-left: 10px solid #e2aa2b91; 298 | background-color: rgba(208, 208, 208, 0); 299 | font-size: 16px; 300 | line-height: 24px; 301 | color: cornflowerblue; 302 | font-weight: bold; 303 | margin: 16px 0; 304 | } 305 | 306 | .TTDF-content h5 { 307 | padding: 0 6px; 308 | border-left: 10px solid #64c1c191; 309 | background-color: rgba(208, 208, 208, 0); 310 | font-size: 15px; 311 | line-height: 21px; 312 | color: cornflowerblue; 313 | font-weight: bold; 314 | margin: 16px 0; 315 | } 316 | 317 | /* 主容器 */ 318 | .TTDF-container { 319 | max-width: 1200px; 320 | margin: 20px auto; 321 | background: #ffffff; 322 | border-radius: 6px; 323 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 324 | overflow: hidden; 325 | border: 1px solid #ddd; 326 | } 327 | 328 | /* 顶部标题栏 */ 329 | .TTDF-header { 330 | background: #2271b1; 331 | color: white; 332 | padding: 14px 24px; 333 | display: flex; 334 | justify-content: space-between; 335 | align-items: center; 336 | gap: 12px; 337 | flex-wrap: wrap; 338 | } 339 | 340 | .TTDF-title { 341 | font-size: 24px; 342 | font-weight: 600; 343 | margin: 0; 344 | } 345 | 346 | .TTDF-title small { 347 | font-size: 16px; 348 | color: rgba(255, 255, 255, 0.8); 349 | font-weight: 400; 350 | margin-left: 8px; 351 | } 352 | 353 | .TTDF-actions { 354 | display: flex; 355 | gap: 12px; 356 | } 357 | 358 | .TTDF-save { 359 | background: rgba(255, 255, 255, 0.2); 360 | color: white; 361 | border: 1px solid rgba(255, 255, 255, 0.3); 362 | padding: 10px 20px; 363 | border-radius: 4px; 364 | cursor: pointer; 365 | transition: background 0.2s; 366 | font-weight: 500; 367 | font-size: 14px; 368 | } 369 | 370 | .TTDF-save:hover { 371 | background: rgba(255, 255, 255, 0.3); 372 | } 373 | 374 | /* 主体内容 */ 375 | .TTDF-body { 376 | display: flex; 377 | min-height: 520px; 378 | background: #fafbfc; 379 | } 380 | 381 | /* 左侧导航 */ 382 | .TTDF-nav { 383 | width: 200px; 384 | background: #f8f9fa; 385 | border-right: 1px solid #ddd; 386 | overflow-y: auto; 387 | max-height: 520px; 388 | } 389 | 390 | .TTDF-nav-item { 391 | display: block; 392 | padding: 12px 20px; 393 | color: #555; 394 | text-decoration: none; 395 | transition: all 0.2s; 396 | border-left: 3px solid transparent; 397 | font-weight: 500; 398 | font-size: 14px; 399 | cursor: pointer; 400 | width: 100%; 401 | text-align: left; 402 | background: transparent; 403 | border: none; 404 | } 405 | 406 | .TTDF-nav-item:hover { 407 | color: #4f46e5; 408 | background: #f0f0f0; 409 | border-left-color: #4f46e5; 410 | } 411 | 412 | .TTDF-nav-item.active { 413 | background: #e8f0fe; 414 | color: #1a73e8; 415 | font-weight: 600; 416 | border-left-color: #4f46e5; 417 | } 418 | 419 | /* 右侧内容区域 */ 420 | .TTDF-content { 421 | flex: 1; 422 | padding: 14px; 423 | overflow-y: auto; 424 | max-height: 520px; 425 | } 426 | 427 | .TTDF-content-card { 428 | border-radius: 4px; 429 | padding: 14px; 430 | border: 1px solid #ddd; 431 | background: #ffffff; 432 | } 433 | 434 | .TTDF-tab-panel { 435 | display: none; 436 | animation: fadeIn 0.3s ease-in-out; 437 | } 438 | 439 | .TTDF-tab-panel.active { 440 | display: block; 441 | } 442 | 443 | @keyframes fadeIn { 444 | from { 445 | opacity: 0; 446 | transform: translateY(10px); 447 | } 448 | 449 | to { 450 | opacity: 1; 451 | transform: translateY(0); 452 | } 453 | } 454 | 455 | /* Element Plus 表单组件适配 */ 456 | .form-group { 457 | margin-bottom: 20px; 458 | } 459 | 460 | .form-group .el-form-item__label { 461 | font-weight: 600; 462 | color: #1f2937; 463 | font-size: 15px; 464 | letter-spacing: -0.2px; 465 | } 466 | 467 | .description { 468 | margin-top: 8px; 469 | font-size: 13px; 470 | color: #6b7280; 471 | line-height: 1.4; 472 | } 473 | 474 | /* AddList 组件样式 */ 475 | .addlist-container { 476 | border: 1px solid #e5e7eb; 477 | border-radius: 6px; 478 | padding: 16px; 479 | background-color: #f9fafb; 480 | } 481 | 482 | .addlist-item { 483 | display: flex; 484 | align-items: center; 485 | gap: 8px; 486 | margin-bottom: 8px; 487 | padding: 8px; 488 | background-color: #ffffff; 489 | border: 1px solid #e5e7eb; 490 | border-radius: 4px; 491 | } 492 | 493 | /* 布局样式 */ 494 | .horizontal-layout { 495 | display: flex; 496 | flex-wrap: wrap; 497 | gap: 16px; 498 | } 499 | 500 | .vertical-layout { 501 | display: flex; 502 | flex-direction: column; 503 | gap: 8px; 504 | } 505 | 506 | /* DialogSelect 对话框样式 */ 507 | .checkbox-list, 508 | .radio-list { 509 | max-height: 300px; 510 | overflow-y: auto; 511 | } 512 | 513 | .dialog-checkbox, 514 | .dialog-radio { 515 | display: block; 516 | margin-bottom: 8px; 517 | padding: 8px; 518 | border-radius: 4px; 519 | transition: background-color 0.2s; 520 | } 521 | 522 | .dialog-checkbox:hover, 523 | .dialog-radio:hover { 524 | background-color: #f3f4f6; 525 | } 526 | 527 | /* Alert 组件样式 */ 528 | .alert { 529 | position: relative; 530 | padding: 0.75rem 1rem 0.75rem 2.5rem; 531 | border-radius: 0.375rem; 532 | font-size: 0.875rem; 533 | line-height: 1.5; 534 | margin: 0.5rem 0; 535 | border-width: 1px; 536 | box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 537 | } 538 | 539 | .alert::before { 540 | content: ""; 541 | position: absolute; 542 | left: 1rem; 543 | top: 50%; 544 | transform: translateY(-50%); 545 | width: 1rem; 546 | height: 1rem; 547 | background-size: contain; 548 | background-repeat: no-repeat; 549 | } 550 | 551 | .alert.info { 552 | background-color: #ebf5ff; 553 | border-color: #d1e7ff; 554 | color: #1c64f2; 555 | } 556 | 557 | .alert.info::before { 558 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%231c64f2'%3E%3Cpath fill-rule='evenodd' d='M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z' clip-rule='evenodd'/%3E%3C/svg%3E"); 559 | } 560 | 561 | .alert.success { 562 | background-color: #f0fdf4; 563 | border-color: #dcfce7; 564 | color: #16a34a; 565 | } 566 | 567 | .alert.success::before { 568 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2316a34a'%3E%3Cpath fill-rule='evenodd' d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z' clip-rule='evenodd'/%3E%3C/svg%3E"); 569 | } 570 | 571 | .alert.warning { 572 | background-color: #fefce8; 573 | border-color: #fef08a; 574 | color: #d97706; 575 | } 576 | 577 | .alert.warning::before { 578 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23d97706'%3E%3Cpath fill-rule='evenodd' d='M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z' clip-rule='evenodd'/%3E%3C/svg%3E"); 579 | } 580 | 581 | .alert.error { 582 | background-color: #fef2f2; 583 | border-color: #fee2e2; 584 | color: #dc2626; 585 | } 586 | 587 | .alert.error::before { 588 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%23dc2626'%3E%3Cpath fill-rule='evenodd' d='M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z' clip-rule='evenodd'/%3E%3C/svg%3E"); 589 | } 590 | 591 | /* 响应式设计 */ 592 | @media (max-width: 768px) { 593 | .TTDF-container { 594 | margin: 10px; 595 | } 596 | 597 | .TTDF-body { 598 | flex-direction: column; 599 | min-height: auto; 600 | } 601 | 602 | .TTDF-nav { 603 | width: 100%; 604 | display: flex; 605 | overflow-x: auto; 606 | padding: 0; 607 | background: #f8f9fa; 608 | border-right: none; 609 | border-bottom: 1px solid #ddd; 610 | max-height: none; 611 | } 612 | 613 | .TTDF-nav-item { 614 | white-space: nowrap; 615 | padding: 10px 16px; 616 | min-width: 100px; 617 | text-align: center; 618 | font-size: 13px; 619 | border-left: none; 620 | border-bottom: 3px solid transparent; 621 | } 622 | 623 | .TTDF-nav-item:hover, 624 | .TTDF-nav-item.active { 625 | border-left-color: transparent; 626 | border-bottom-color: #4f46e5; 627 | } 628 | 629 | .TTDF-title { 630 | font-size: 20px; 631 | } 632 | 633 | .TTDF-content { 634 | padding: 16px; 635 | max-height: none; 636 | } 637 | 638 | .horizontal-layout { 639 | flex-direction: column; 640 | gap: 8px; 641 | } 642 | 643 | .addlist-item { 644 | flex-direction: column; 645 | gap: 8px; 646 | align-items: stretch; 647 | } 648 | } 649 | 650 | /* 现代化字体系统 */ 651 | h1, 652 | h2, 653 | h3, 654 | h4, 655 | h5, 656 | h6 { 657 | font-weight: 700; 658 | letter-spacing: -0.5px; 659 | line-height: 1.2; 660 | } 661 | 662 | /* 加载状态 */ 663 | .ttdf-loading { 664 | position: fixed; 665 | top: 0; 666 | left: 0; 667 | width: 100%; 668 | height: 100%; 669 | background: rgba(0, 0, 0, 0.6); 670 | z-index: 9999; 671 | display: none; 672 | justify-content: center; 673 | align-items: center; 674 | backdrop-filter: blur(8px); 675 | } 676 | 677 | .ttdf-loading-spinner { 678 | width: 48px; 679 | height: 48px; 680 | border: 3px solid rgba(255, 255, 255, 0.2); 681 | border-top: 3px solid #4f46e5; 682 | border-radius: 50%; 683 | animation: modernSpin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite; 684 | box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); 685 | } 686 | 687 | @keyframes modernSpin { 688 | 0% { 689 | transform: rotate(0deg) scale(1); 690 | } 691 | 692 | 50% { 693 | transform: rotate(180deg) scale(1.1); 694 | } 695 | 696 | 100% { 697 | transform: rotate(360deg) scale(1); 698 | } 699 | } 700 | 701 | /* Element Plus 组件覆盖样式 */ 702 | .el-form-item { 703 | margin-bottom: 20px; 704 | } 705 | 706 | .el-form-item__label { 707 | font-weight: 600 !important; 708 | color: #1f2937 !important; 709 | font-size: 15px !important; 710 | } 711 | 712 | .el-input__wrapper { 713 | border-radius: 4px; 714 | } 715 | 716 | .el-button { 717 | border-radius: 4px; 718 | } 719 | 720 | .el-dialog { 721 | border-radius: 8px; 722 | } 723 | 724 | .el-dialog__header { 725 | background: #f8f9fa; 726 | border-bottom: 1px solid #e5e7eb; 727 | } 728 | 729 | .el-dialog__title { 730 | font-weight: 600; 731 | color: #1f2937; 732 | } 733 | 734 | /* 消息提示动画 */ 735 | .ttdf-message { 736 | animation: slideIn 0.3s ease-out; 737 | } 738 | 739 | @keyframes slideIn { 740 | from { 741 | opacity: 0; 742 | transform: translateY(-10px); 743 | } 744 | 745 | to { 746 | opacity: 1; 747 | transform: translateY(0); 748 | } 749 | } --------------------------------------------------------------------------------