├── .gitattributes ├── .gitignore ├── 404.php ├── LICENSE ├── README.md ├── archive.php ├── assets ├── main.css ├── main.js └── normalize.css ├── functions.php ├── includes ├── footer.php ├── head.php └── header.php ├── index.php ├── libs ├── Contents.php └── Utils.php ├── page.php └── post.php /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=PHP 2 | *.css linguist-language=PHP 3 | *.html linguist-language=PHP -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | temp/* 3 | .DS_Store 4 | package-lock.json -------------------------------------------------------------------------------- /404.php: -------------------------------------------------------------------------------- 1 | need('includes/head.php'); 11 | $this->need('includes/header.php'); 12 | ?> 13 | 14 | 15 | 16 | need('includes/footer.php'); ?> -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 AlanDecode 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一个 Typecho 主题开发起步框架 2 | 3 | 我在为 Typecho 写主题时发现了几个问题: 4 | 5 | - 某些代码可以复用,没必要每次都写一遍 6 | - Typecho 文档不充分,某些方法每次用到都要现查 7 | - 某些方法 Typecho 的接口太复杂或者压根就没有实现 8 | - 某些方法在不同的 Typecho 版本行为不一致 9 | - 我的主题都有比较相似的目录结构 10 | 11 | 因此我准备把自己的最佳实践打包一下,美其名曰一个「框架」,其实只是为了减少一些无谓的工作量。 12 | 13 | ~~是不是闻到了 JQuery 的味道~~ 14 | 15 | 使用时,直接下载本框架,并在其中删改、添加代码。Utils.php 与 Contents.php 中提供了一些常用的代码,可以看看。 16 | 17 | **非常欢迎各种 pull request** 18 | 19 | 目录结构: 20 | 21 | ``` 22 | index.php 23 | post.php 24 | page.php 25 | archive.php 26 | 404.php 27 | functions.php 28 | includes 29 | |-- head.php 30 | |-- header.php 31 | |-- footer.php 32 | libs 33 | |-- Utils.php 34 | |-- Contents.php 35 | assets 36 | |-- main.css 37 | |-- main.js 38 | ``` 39 | 40 | 41 | ## License 42 | 43 | MIT © [AlanDecode](https://github.com/AlanDecode) -------------------------------------------------------------------------------- /archive.php: -------------------------------------------------------------------------------- 1 | need('includes/head.php'); 11 | $this->need('includes/header.php'); 12 | ?> 13 | 14 | 15 | 16 | need('includes/footer.php'); ?> -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanDecode/typecho-theme-dev-framework/620fae9f54cc589327f55f38b16112677c484b09/assets/main.css -------------------------------------------------------------------------------- /assets/main.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlanDecode/typecho-theme-dev-framework/620fae9f54cc589327f55f38b16112677c484b09/assets/main.js -------------------------------------------------------------------------------- /assets/normalize.css: -------------------------------------------------------------------------------- 1 | html{line-height:1.15;-webkit-text-size-adjust:100%;}body{margin:0;}main{display:block;}h1{font-size:2em;margin:0.67em 0;}hr{box-sizing:content-box;height:0;overflow:visible;}pre{font-family:monospace,monospace;font-size:1em;}a{background-color:transparent;}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted;}b,strong{font-weight:bolder;}code,kbd,samp{font-family:monospace,monospace;font-size:1em;}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sub{bottom:-0.25em;}sup{top:-0.5em;}img{border-style:none;}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0;}button,input{overflow:visible;}button,select{text-transform:none;}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button;}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0;}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText;}fieldset{padding:0.35em 0.75em 0.625em;}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal;}progress{vertical-align:baseline;}textarea{overflow:auto;}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0;}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto;}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px;}[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit;}details{display:block;}summary{display:list-item;}template{display:none;}[hidden]{display:none;} -------------------------------------------------------------------------------- /functions.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | contentEx = array('Contents','parseContent'); 23 | Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array('Contents','parseContent'); 24 | 25 | /** 26 | * 主题启用时执行的方法 27 | */ 28 | function themeInit() { 29 | /** 30 | * 重置某些设置项,采用数据库查询方式完成 31 | */ 32 | $db = Typecho_Db::get(); 33 | 34 | /* 关闭评论反垃圾保护,使用 PJAX 时可能需要取消注释以下 4 行 */ 35 | // $query = $db->update('table.options')->rows(array('value'=>'0'))->where('name=?', 'commentsAntiSpam'); 36 | // $db->query($query); 37 | // $query = $db->update('table.options')->rows(array('value'=>'0'))->where('name=?', 'commentsCheckReferer'); 38 | // $db->query($query); 39 | 40 | /* 设置评论最大嵌套层数 */ 41 | $query = $db->update('table.options')->rows(array('value'=>'999'))->where('name=?', 'commentsMaxNestingLevels'); 42 | $db->query($query); 43 | 44 | /* 强制新评论在前 */ 45 | $query = $db->update('table.options')->rows(array('value'=>'DESC'))->where('name=?', 'commentsOrder'); 46 | $db->query($query); 47 | 48 | /* 默认显示第一页评论 */ 49 | $query = $db->update('table.options')->rows(array('value'=>'first'))->where('name=?', 'commentsPageDisplay'); 50 | $db->query($query); 51 | } 52 | 53 | /** 54 | * 主题后台设置 55 | */ 56 | function themeConfig($form) { 57 | 58 | } 59 | 60 | /** 61 | * 文章与独立页自定义字段 62 | */ 63 | function themeFields(Typecho_Widget_Helper_Layout $layout) { 64 | 65 | } -------------------------------------------------------------------------------- /includes/footer.php: -------------------------------------------------------------------------------- 1 | 标签,并关闭 、 标签 6 | * 7 | * @author 熊猫小A 8 | */ 9 | if (!defined('__TYPECHO_ROOT_DIR__')) exit; 10 | ?> 11 | 12 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /includes/head.php: -------------------------------------------------------------------------------- 1 | 标签,并开始 、 标签 6 | * 7 | * @author 熊猫小A 8 | */ 9 | if (!defined('__TYPECHO_ROOT_DIR__')) exit; 10 | ?> 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | is('post') || $this->is('page')){ 24 | if(isset($this->fields->banner)) 25 | $banner=$this->fields->banner; 26 | if(isset($this->fields->excerpt)) 27 | $description = $this->fields->excerpt; 28 | }else{ 29 | $description = Helper::options()->description; 30 | } 31 | ?> 32 | <?php Contents::title($this); ?> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | " /> 47 | header('description=&'); ?> 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /includes/header.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 熊猫小A 6 | * 7 | * @package Typecho-Theme-X 8 | * @author 熊猫小A 9 | * @version 1.0 10 | * @link https://blog.imalan.cn 11 | */ 12 | if (!defined('__TYPECHO_ROOT_DIR__')) exit; 13 | $this->need('includes/head.php'); 14 | $this->need('includes/header.php'); 15 | ?> 16 | 17 | 18 | 19 | need('includes/footer.php'); ?> -------------------------------------------------------------------------------- /libs/Contents.php: -------------------------------------------------------------------------------- 1 | 'cid', 43 | 'Comments' => 'coid', 44 | 'Metas' => 'mid', 45 | 'Users' => 'uid' 46 | ); 47 | $className = "Widget_Abstract_{$table}"; 48 | $key = $keys[$table]; 49 | $db = Typecho_Db::get(); 50 | $widget = new $className(Typecho_Request::getInstance(), Typecho_Widget_Helper_Empty::getInstance()); 51 | 52 | $db->fetchRow( 53 | $widget->select()->where("{$key} = ?", $pkId)->limit(1), 54 | array($widget, 'push')); 55 | return $widget; 56 | } 57 | 58 | /** 59 | * 输出完备的标题 60 | */ 61 | public static function title(Widget_Archive $archive) 62 | { 63 | $archive->archiveTitle(array( 64 | 'category' => '分类 %s 下的文章', 65 | 'search' => '包含关键字 %s 的文章', 66 | 'tag' => '标签 %s 下的文章', 67 | 'author' => '%s 发布的文章' 68 | ), '', ' - '); 69 | Helper::options()->title(); 70 | } 71 | 72 | /** 73 | * 返回上一篇文章 74 | */ 75 | public static function getPrev($archive) 76 | { 77 | $db = Typecho_Db::get(); 78 | $content = $db->fetchRow($db->select()->from('table.contents')->where('table.contents.created < ?', $archive->created) 79 | ->where('table.contents.status = ?', 'publish') 80 | ->where('table.contents.type = ?', $archive->type) 81 | ->where('table.contents.password IS NULL') 82 | ->order('table.contents.created', Typecho_Db::SORT_DESC) 83 | ->limit(1)); 84 | 85 | if($content) { 86 | return self::widgetById('Contents', $content['cid']); 87 | }else{ 88 | return NULL; 89 | } 90 | } 91 | 92 | /** 93 | * 返回下一篇文章 94 | */ 95 | public static function getNext($archive) 96 | { 97 | $db = Typecho_Db::get(); 98 | $content = $db->fetchRow($db->select()->from('table.contents')->where('table.contents.created > ? AND table.contents.created < ?', 99 | $archive->created, Helper::options()->gmtTime) 100 | ->where('table.contents.status = ?', 'publish') 101 | ->where('table.contents.type = ?', $archive->type) 102 | ->where('table.contents.password IS NULL') 103 | ->order('table.contents.created', Typecho_Db::SORT_ASC) 104 | ->limit(1)); 105 | 106 | if($content) { 107 | return self::widgetById('Contents', $content['cid']); 108 | }else{ 109 | return NULL; 110 | } 111 | } 112 | 113 | /** 114 | * 最近评论,过滤引用通告,过滤博主评论 115 | */ 116 | public static function getRecentComments($num = 10) 117 | { 118 | $comments = array(); 119 | 120 | $db = Typecho_Db::get(); 121 | $rows = $db->fetchAll($db->select()->from('table.comments')->where('table.comments.status = ?', 'approved') 122 | ->where('type = ?', 'comment') 123 | ->where('ownerId <> authorId') 124 | ->order('table.comments.created', Typecho_Db::SORT_DESC) 125 | ->limit($num)); 126 | 127 | foreach ($rows as $row) { 128 | $comment = self::widgetById('Comments', $row['coid']); 129 | $comments[] = $comment; 130 | } 131 | 132 | return $comments; 133 | } 134 | } -------------------------------------------------------------------------------- /libs/Utils.php: -------------------------------------------------------------------------------- 1 | index($path); 19 | } 20 | 21 | /** 22 | * 输出相对首页路径,本方法用于静态文件 23 | */ 24 | public static function indexHome($path = '') 25 | { 26 | Helper::options()->siteUrl($path); 27 | } 28 | 29 | /** 30 | * 输出相对主题目录路径,用于静态文件 31 | */ 32 | public static function indexTheme($path = '') 33 | { 34 | Helper::options()->themeUrl($path); 35 | } 36 | 37 | /** 38 | * 根据邮箱返回 Gravatar 头像链接 39 | */ 40 | public static function gravatar($mail, $size = 64, $d = '') 41 | { 42 | return Typecho_Common::gravatarUrl($mail, $size, '', urlencode($d), true); 43 | } 44 | 45 | /** 46 | * 判断插件是否可用(存在且已激活) 47 | */ 48 | public static function hasPlugin($name) 49 | { 50 | $plugins = Typecho_Plugin::export(); 51 | $plugins = $plugins['activated']; 52 | return is_array($plugins) && array_key_exists($name, $plugins); 53 | } 54 | 55 | /** 56 | * 判断移动端请求 57 | */ 58 | public static function isMobile() 59 | { 60 | if (isset ($_SERVER['HTTP_X_WAP_PROFILE'])){ 61 | return TRUE; 62 | } 63 | 64 | if (isset ($_SERVER['HTTP_USER_AGENT'])) { 65 | $clientkeywords = array ('mobile','nokia','sony','ericsson','mot','samsung','htc','sgh','lg','sharp','sie-','philips','panasonic','alcatel','lenovo','iphone','ipod','blackberry','meizu','android','netfront','symbian','ucweb','windowsce','palm','operamini','operamobi','openwave','nexusone','cldc','midp','wap' 66 | ); 67 | if (preg_match("/(" . implode('|', $clientkeywords) . ")/i", strtolower($_SERVER['HTTP_USER_AGENT']))){ 68 | return TRUE; 69 | } 70 | } 71 | if (isset ($_SERVER['HTTP_ACCEPT'])){ 72 | if ((strpos($_SERVER['HTTP_ACCEPT'], 'vnd.wap.wml') !== FALSE) && (strpos($_SERVER['HTTP_ACCEPT'], 'text/html') === FALSE || (strpos($_SERVER['HTTP_ACCEPT'], 'vnd.wap.wml') < strpos($_SERVER['HTTP_ACCEPT'], 'text/html')))){ 73 | return TRUE; 74 | } 75 | } 76 | return FALSE; 77 | } 78 | 79 | /** 80 | * 判定电脑端微信内置浏览器 81 | */ 82 | public static function isWeixin() 83 | { 84 | return !self::isMobile() && strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger') !== false; 85 | } 86 | 87 | /** 88 | * 判定内容是否过时 89 | * $timeout:过期时间,单位为天 90 | */ 91 | public static function isOutdated($archive, $timeout) 92 | { 93 | date_default_timezone_set("Asia/Shanghai"); 94 | return round((time()- $archive->created) / 3600 / 24) > $timeout; 95 | } 96 | 97 | /** 98 | * 输出建站时间(最早一篇文章的写作时间) 99 | */ 100 | public static function getBuildTime() 101 | { 102 | date_default_timezone_set("Asia/Shanghai"); 103 | $db = Typecho_Db::get(); 104 | $content = $db->fetchRow($db->select()->from('table.contents') 105 | ->where('table.contents.status = ?', 'publish') 106 | ->where('table.contents.password IS NULL') 107 | ->order('table.contents.created', Typecho_Db::SORT_ASC) 108 | ->limit(1)); 109 | return $content['created']; 110 | } 111 | 112 | /** 113 | * 已发布文章数量 114 | */ 115 | public static function getPostNum() 116 | { 117 | $db = Typecho_Db::get(); 118 | return $db->fetchObject($db->select(array('COUNT(cid)' => 'num')) 119 | ->from('table.contents') 120 | ->where('table.contents.type = ?', 'post') 121 | ->where('table.contents.status = ?', 'publish'))->num; 122 | } 123 | 124 | /** 125 | * 分类数量 126 | */ 127 | public static function getCatNum() 128 | { 129 | $db = Typecho_Db::get(); 130 | return $db->fetchObject($db->select(array('COUNT(mid)' => 'num')) 131 | ->from('table.metas') 132 | ->where('table.metas.type = ?', 'category'))->num; 133 | } 134 | 135 | /** 136 | * 标签数量 137 | */ 138 | public static function getTagNum() 139 | { 140 | $db = Typecho_Db::get(); 141 | return $db->fetchObject($db->select(array('COUNT(mid)' => 'num')) 142 | ->from('table.metas') 143 | ->where('table.metas.type = ?', 'tag'))->num; 144 | } 145 | 146 | /** 147 | * 总字数 148 | * 149 | * @return int 150 | */ 151 | public static function getWordCount() 152 | { 153 | $db = Typecho_Db::get(); 154 | $posts = $db->fetchAll($db->select('table.contents.text') 155 | ->from('table.contents') 156 | ->where('table.contents.type = ?', 'post') 157 | ->where('table.contents.status = ?', 'publish')); 158 | $total = 0; 159 | foreach ($posts as $post) { 160 | $total = $total + mb_strlen(preg_replace("/[^\x{4e00}-\x{9fa5}]/u", "", $post['text']), 'UTF-8'); 161 | } 162 | return $total; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /page.php: -------------------------------------------------------------------------------- 1 | need('includes/head.php'); 11 | $this->need('includes/header.php'); 12 | ?> 13 | 14 | 15 | 16 | need('includes/footer.php'); ?> -------------------------------------------------------------------------------- /post.php: -------------------------------------------------------------------------------- 1 | need('includes/head.php'); 11 | $this->need('includes/header.php'); 12 | ?> 13 | 14 | 15 | 16 | need('includes/footer.php'); ?> --------------------------------------------------------------------------------